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

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

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

本系列源码地址

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

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

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

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

public class ValuesController : ControllerBase
{
    private readonly MyOptions _options1;
    private readonly MyOptions _options2;
    private readonly MyOptions _options3;
    private readonly IConfiguration _configurationRoot;

    public ValuesController(IConfiguration configurationRoot, IOptionsMonitor options1, IOptionsSnapshot options2, 
        IOptions options3 )
    {
        //IConfiguration(ConfigurationRoot)随着配置文件进行更新(需要IConfigurationProvider监听配置源的更改)
        _configurationRoot = configurationRoot;
        //单例,监听IConfiguration的IChangeToken,在配置源发生改变时,自动删除缓存
        //生成新的Option实例并绑定,加入缓存
        _options1 = options1.CurrentValue;
        //scoped,每次请求重新生成Option实例并从IConfiguration获取数据进行绑定
        _options2 = options2.Value;
        //单例,从IConfiguration获取数据进行绑定,只绑定一次
        _options3 = options3.Value;
    }
}

二、源码解读

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

public static IServiceCollection Configure(this IServiceCollection services, IConfiguration config) where TOptions : class
    => services.Configure(Options.Options.DefaultName, config, _ => { });
    
public static IServiceCollection Configure(this IServiceCollection services, string name, IConfiguration config, Action configureBinder)
    where TOptions : class
{
    services.AddOptions();
    
    services.AddSingleton>(new ConfigurationChangeTokenSource(name, config));
    
    return services.AddSingleton>(new NamedConfigureFromConfigurationOptions(name, config, configureBinder));
}
/// 为IConfigurationSection实例注册需要绑定的TOptions
public static IServiceCollection AddOptions(this IServiceCollection 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;
}

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

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

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

services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));

services.AddSingleton>(new NamedConfigureFromConfigurationOptions(name, config, configureBinder));

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

1.1 OptionsManager 的实现

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

public class OptionsManager : IOptions, IOptionsSnapshot where TOptions : class
{
    private readonly IOptionsFactory _factory;

    // 单例OptionsManager的私有缓存,通过ConcurrentDictionary实现了 IOptionsMonitorCache接口
    // Di中注入的单例OptionsCache<> 是给 OptionsMonitor<>使用的
    private readonly OptionsCache _cache = new OptionsCache(); // Note: this is a private cache

    public OptionsManager(IOptionsFactory factory)
    {
        _factory = factory;
    }

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

1.2 IOptionsFactory 的实现

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

/// Options工厂类 生命周期:Transient
/// 单例OptionsManager和单例OptionsMonitor持有不同的工厂实例
public class OptionsFactory : IOptionsFactory where TOptions : class
{
    private readonly IEnumerable> _setups;
    private readonly IEnumerable> _postConfigures;

    public OptionsFactory(IEnumerable> setups, IEnumerable> postConfigures)
    {
        _setups = setups;
        _postConfigures = postConfigures;
    }

    public TOptions Create(string name)
    {
        var options = CreateInstance(name);
        foreach (var setup in _setups)
        {
            if (setup is IConfigureNamedOptions namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        foreach (var post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        return options;
    }

    protected virtual TOptions CreateInstance(string name)
    {
        return Activator.CreateInstance();
    }
}

1.3 NamedConfigureFromConfigurationOptions 的实现

在内部通过Action 委托,保存了IConfiguration.Bind()方法。该方法实现了从IConfigurationTOptions实例的赋值。
此处合并了NamedConfigureFromConfigurationOptions ConfigureNamedOptions 的代码。

public class NamedConfigureFromConfigurationOptions : ConfigureNamedOptions
    where TOptions : class
{
    public NamedConfigureFromConfigurationOptions(string name, IConfiguration config)
        : this(name, config, _ => { })
    { }

    public NamedConfigureFromConfigurationOptions(string name, IConfiguration config, Action configureBinder)
        : this(name, options => config.Bind(options, configureBinder))
    { }
    
    public ConfigureNamedOptions(string name, Action action)
    {
        Name = name;
        Action = action;
    }

    public string Name { get; }

    public Action Action { get; }

    public virtual void Configure(string name, TOptions options)
    {
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }

    public void Configure(TOptions options) => Configure(string.Empty, options);
}

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

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

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

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

与其有关的注入有五个:

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));

services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));

services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));

services.AddSingleton>(new ConfigurationChangeTokenSource(name, config));

services.AddSingleton>(new NamedConfigureFromConfigurationOptions(name, config, configureBinder));

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

graph LR
A["FileProviders"]--IChangeToken-->B
B["IConfigurationProvider"]--IChangeToken-->C["IConfigurationRoot"]

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

3.1 ConfigurationChangeTokenSource 的实现

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

public class ConfigurationChangeTokenSource : IOptionsChangeTokenSource
{
    private IConfiguration _config;

    public ConfigurationChangeTokenSource(IConfiguration config) : this(string.Empty, config)
    { }

    public ConfigurationChangeTokenSource(string name, IConfiguration config)
    {
        _config = config;
        Name = name ?? string.Empty;
    }

    public string Name { get; }

    public IChangeToken GetChangeToken()
    {
        return _config.GetReloadToken();
    }
}

3.2 OptionsMonitor 的实现

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

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

        public OptionsMonitor(IOptionsFactory factory, IEnumerable> sources, IOptionsMonitorCache 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);
            }
        }

        public TOptions CurrentValue
        {
            get => Get(Options.DefaultName);
        }

        public virtual TOptions Get(string name)
        {
            name = name ?? Options.DefaultName;
            return _cache.GetOrAdd(name, () => _factory.Create(name));
        }

        public IDisposable OnChange(Action listener)
        {
            var disposable = new ChangeTrackerDisposable(this, listener);
            _onChange += disposable.OnChange;
            return disposable;
        }

        public void Dispose()
        {
            foreach (var registration in _registrations)
            {
                registration.Dispose();
            }

            _registrations.Clear();
        }

        internal class ChangeTrackerDisposable : IDisposable
        {
            private readonly Action _listener;
            private readonly OptionsMonitor _monitor;

            public ChangeTrackerDisposable(OptionsMonitor monitor, Action listener)
            {
                _listener = listener;
                _monitor = monitor;
            }

            public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);

            public void Dispose() => _monitor._onChange -= OnChange;
        }
    }

4. 测试代码

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

public class ConfigurationTest
{
    public static void Run()
    {
        var builder = new ConfigurationBuilder();
        builder.AddJsonFile(null, $@"C:\WorkStation\Code\GitHubCode\CoreApp\CoreWebApp\appsettings.json", true,true);
        var configuration = builder.Build();
        Task.Run(() => {
            ChangeToken.OnChange(() => configuration.GetReloadToken(), () => {
                Console.WriteLine("Configuration has changed");
            });
        });
        var optionsChangeTokenSource = new ConfigurationChangeTokenSource(configuration);
        var configureOptions = new NamedConfigureFromConfigurationOptions(string.Empty, configuration);
        var optionsFactory = new OptionsFactory(new List>() { configureOptions },new List>());
        var optionsMonitor = new OptionsMonitor(optionsFactory,new List>() { optionsChangeTokenSource },new OptionsCache());
        optionsMonitor.OnChange((option,name) => {
            Console.WriteLine($@"optionsMonitor Detected Configuration has changed,current Value is {option.TestOption}");
        });
        Thread.Sleep(600000);
    }
}

测试结果

回调会触发两次,这是由于FileSystemWatcher造成的,可以通过设置一个后台线程,在检测到文件变化时,主线程将标志位置true,后台线程轮询标志位
Asp.NetCore源码学习[1-2]:配置[Option]_第1张图片
---

结语

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

你可能感兴趣的:(Asp.NetCore源码学习[1-2]:配置[Option])