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


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

    在Asp. NetCore中,配置系统支持不同的配置源(文件、环境变量等),虽然有多种的配置源,但是最终提供给系统使用的只有一个对象,那就是ConfigurationRoot。其内部维护了一个集合,用于保存各种配置源的IConfigurationProviderIConfigurationProvider提供了对配置源的实际访问。当通过key去ConfigurationRoot查找对应的Value时,实际上会通过遍历IConfigurationProvider去查找对应的键值。 本篇文章主要描述ConfigurationRoot对象的构建过程。

    本系列源码地址

    1. Asp.NetCore 入口点代码

    CreateWebHostBuilder(args).Build().Run();
    

    2. Asp.NetCore 部分源码

    WebHostBuilder内部维护了_configureAppConfigurationBuilder字段,其类型是 Action<WebHostBuilderContext, IConfigurationBuilder>,该委托用于对ConfigurationBuilder进行配置。首先在构造函数中先将环境变量的配置加载到 _config 字段中,用于设置默认监听目录为程序执行目录。CreateDefaultBuilder方法中通过调用ConfigureAppConfiguration方法保存委托,然后在Build方法中构建配置系统目标类ConfigurationRoot,最后通过单例模式注入到依赖系统中。

    public class WebHostBuilder
    {
        private Action<WebHostBuilderContext, IConfigurationBuilder> _configureAppConfigurationBuilder;
        
        private IConfiguration _config;
        
        public WebHostBuilder()
        {
            _hostingEnvironment = new HostingEnvironment();
            /// 
            _config = new ConfigurationBuilder()
                .AddEnvironmentVariables(prefix: "ASPNETCORE_")
                .Build();
        }
        
        public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)
        {
            _configureAppConfigurationBuilder += configureDelegate;
            return this;
        }
        
        public IWebHost Build()
        {
            var builder = new ConfigurationBuilder();
            //通过委托配置IConfigurationBuilder
            _configureAppConfigurationBuilder?.Invoke(_context, builder);
            //构建ConfigurationRoot
            var configuration = builder.Build();
            // register configuration as factory to make it dispose with the service provider
            services.AddSingleton<IConfiguration>(_ => configuration);
        }
    }
    public static IWebHostBuilder CreateDefaultBuilder(string[] args)
    {
        var builder = new WebHostBuilder();
        builder.ConfigureAppConfiguration((hostingContext, config) =>
        {
            //为 IConfigurationBuilder 注册配置源(JsonConfigurationSource)
            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
        });
        return builder;
    }
    
    

    3. 参照以上 Asp.NetCore 代码,写静态测试方法

        public class ConfigurationTest
        {
            public static void Run()
            {
                //1.实例化ConfigurationBuilder
                var builder = new ConfigurationBuilder();
                //2.增加配置源
                builder.AddJsonFile(null, "appsettings.json", true,true);
                //3.构建ConfigurationRoot对象
                var configuration = builder.Build();
                //观察ConfigurationRoot是否发生更改
                Task.Run(() => {
                    ChangeToken.OnChange(() => configuration.GetReloadToken(), () => {
                        Console.WriteLine("Configuration has changed");
                    });
                });
                Thread.Sleep(60000);
            }
        }
    

    4. 通过ConfigurationBuilder类构建目标类ConfigurationRoot

    ConfigurationBuilder是配置系统的构建类,通过Build方法构建配置系统的目标类ConfigurationRoot。其维护了一个用于保存IConfigurationSource的集合,IConfigurationSource用于提供IConfigurationProvider。在Build方法中,遍历IList构建IConfigurationProvider对象,然后将IConfigurationProvider集合传到ConfigurationRoot的构造函数中。代码如下:

        /// <summary>
        /// 配置系统构建类
        /// </summary>
        public class ConfigurationBuilder : IConfigurationBuilder
        {
            /// 配置源集合
            public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
    
            /// 增加一个新的配置源
            public IConfigurationBuilder Add(IConfigurationSource source)
            {
                if (source == null)
                {
                    throw new ArgumentNullException(nameof(source));
                }
                Sources.Add(source);
                return this;
            }
    
            /// 通过配置源中提供的IConfigurationProvider构建配置根对象ConfigurationRoot
            public IConfigurationRoot Build()
            {
                var providers = new List<IConfigurationProvider>();
                foreach (var source in Sources)
                {
                    var provider = source.Build(this);
                    providers.Add(provider);
                }
                return new ConfigurationRoot(providers);
            }
        }
    

    IConfigurationSource对象不仅仅用于创建IConfigurationProvider,还保存了构建IConfigurationProvider需要的依赖和配置选项。


    4.1 ConfigurationRoot 类实现

    该类通过IList进行初始化。其内部维护了类型为ConfigurationReloadToken的字段,该字段提供给外部,来进行所有配置源的监听。每个IConfigurationProvider对象同样维护了类型为ConfigurationReloadToken的字段。当IConfigurationProvider监测到配置源发生更改时,更改IConfigurationProvider.IChangeToken的状态
    在构造函数中执行以下操作:

    • 1 调用IConfigurationProvider.Load()从配置源(文件、环境变量等)加载配置项
    • 2 通过ChangeToken.OnChange()方法 监听每个IConfigurationProvider.IChangeToken的状态改变,当其状态发生改变时更改ConfigurationRoot.IChangeToken的状态。(在ConfigurationRoot外部可以通过监听IChangeToken状态的改变,得知配置源发生了改变)
        /// <summary>
        /// 配置系统的根节点
        /// </summary>
        public class ConfigurationRoot : IConfigurationRoot, IDisposable
        {
            private readonly IList<IConfigurationProvider> _providers;
            private readonly IList<IDisposable> _changeTokenRegistrations;
            private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
    
            /// <summary>
            /// 使用IConfigurationProvider集合初始化ConfigurationRoot
            /// </summary>
            /// <param name="providers">The <see cref="IConfigurationProvider"/>s for this configuration.</param>
            public ConfigurationRoot(IList<IConfigurationProvider> providers)
            {
                _providers = providers ?? throw new ArgumentNullException(nameof(providers));
    
                _changeTokenRegistrations = new List<IDisposable>(providers.Count);
                foreach (var p in providers)
                {
                    p.Load();
                    //将每个IConfigurationProvider的change token与ConfigurationRoot 的change token绑定
                    //当IConfigurationProvider._cts.Cancel()触发时,触发当ConfigurationRoot._cts.Cancel()
                    _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
                }
            }
    
            public IEnumerable<IConfigurationProvider> Providers => _providers;
    
            /// 遍历_providers来设置、获取配置项的键值对
            public string this[string key]
            {
                get
                {
                    for (var i = _providers.Count - 1; i >= 0; i--)
                    {
                        var provider = _providers[i];
    
                        if (provider.TryGet(key, out var value))
                        {
                            return value;
                        }
                    }
    
                    return null;
                }
                set
                {
                    if (!_providers.Any())
                    {
                        throw new InvalidOperationException("Can't find any IConfigurationProvider");
                    }
    
                    foreach (var provider in _providers)
                    {
                        provider.Set(key, value);
                    }
                }
            }
    
            /// 获取IChangeToken,用于供外部使用者收到配置改变的消息通知 
            public IChangeToken GetReloadToken() => _changeToken;
    
            public void Reload()
            {
                foreach (var provider in _providers)
                {
                    provider.Load();
                }
                RaiseChanged();
            }
    
            /// 生成一个新的change token,并触发ConfigurationRoot的change token(旧)状态改变
            private void RaiseChanged()
            {
                var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
                previousToken.OnReload();
            }
    
            /// <inheritdoc />
            public void Dispose()
            {
                // dispose change token registrations
                foreach (var registration in _changeTokenRegistrations)
                {
                    registration.Dispose();
                }
    
                // dispose providers
                foreach (var provider in _providers)
                {
                    (provider as IDisposable)?.Dispose();
                }
            }
        }
    

    4.2 ConfigurationReloadToken 的实现

    其使用适配器模式,通过CancellationTokenSource实现IChangeToken接口。代码如下:

        /// <summary>
        /// 用于发送更改通知
        /// </summary>
        public interface IChangeToken
        {
            /// 指示是否发生更改
            bool HasChanged { get; }
    
            /// 指示token是否会主动调用callbacks,false的情况下:token的消费者需要轮询 HasChanged 属性检测是否发生更改
            bool ActiveChangeCallbacks { get; }
    
            /// 注册回调函数, 更改发生时(HasChanged为true),会被调用(只会被调用一次)
            IDisposable RegisterChangeCallback(Action<object> callback, object state);
        }
    
        /// 基于CancellationTokenSource实现IChangeToken接口(适配器模式)
        public class ConfigurationReloadToken:IChangeToken
        {
            private CancellationTokenSource _cts = new CancellationTokenSource();
    
            /// CancellationTokenSource会主动调用callbacks,所以为true
            public bool ActiveChangeCallbacks => true;
    
            public bool HasChanged => _cts.IsCancellationRequested;
    
            public IDisposable RegisterChangeCallback(Action<object> callback, object state) => _cts.Token.Register(callback, state);
    
            public void OnReload() => _cts.Cancel();
        }
    

    4.3 简述CancellationTokenSource 对象

    基于协作取消模式设计的对象,用于取消异步操作或者长时间同步操作。( .NET指南/取消托管线程)

    image

    CancellationTokenSource对象的特点:

    • 1 CancellationTokenSource.Token是值类型,传递副本
    • 2 调用 CancellationTokenSource.Cancel 方法提供取消通知后,CancellationTokenSource.Token的状态发生改变,调用callbacks,并改变所有Token副本的状态
    • 3 需要调用dispose释放CancellationTokenSource
    • 4 多次调用CancellationTokenSource.Cancel,callbacks也只会执行一次
    • 5 再CancellationTokenSource.Cancel之后,新注册的callback同样也会被执行

    4.4 通过ChangeToken.OnChange 静态方法实现更改通知的持续消费

    由于CancellationTokenSource.Cancel只会触发一次callbacks,需要ChangeToken.OnChange来实现持续监听取消通知。
    实现原理:每次需要发生更改通知时,首先生成一个新的cts,然后改变旧的cts状态,触发回调函数,最后将新的cts与回调函数绑定。

        /// <summary>
        /// 将changeToken消费者注册到IChangeToken的回调函数中,并实现IChangeToken状态改变的持续消费
        /// </summary>
        public static class ChangeToken
        {
            /// 为changetoken生产者绑定消费者. 
            /// 1.在IChangeToken的状态未改变的情况下,生产者每次返回相同的IChangeToken
            /// 2.状态改变时,生产者生成新的IChangeToken,消费者执行响应动作,为新的IChangeToken绑定消费者,释放旧的IChangeToken
            public static IDisposable OnChange(Func<IChangeToken> changeTokenProducer, Action changeTokenConsumer)
            {
                if (changeTokenProducer == null)
                {
                    throw new ArgumentNullException(nameof(changeTokenProducer));
                }
                if (changeTokenConsumer == null)
                {
                    throw new ArgumentNullException(nameof(changeTokenConsumer));
                }
                return new ChangeTokenRegistration<Action>(changeTokenProducer, callback => callback(), changeTokenConsumer);
            }
    
            private class ChangeTokenRegistration<TState> : IDisposable
            {
                private readonly Func<IChangeToken> _changeTokenProducer;
                private readonly Action<TState> _changeTokenConsumer;
                private readonly TState _state;
                private IDisposable _disposable;//用于保存当前正在使用的 IChangeToken
    
                private static readonly NoopDisposable _disposedSentinel = new NoopDisposable();
    
                public ChangeTokenRegistration(Func<IChangeToken> changeTokenProducer, Action<TState> changeTokenConsumer, TState state)
                {
                    _changeTokenProducer = changeTokenProducer;
                    _changeTokenConsumer = changeTokenConsumer;
                    _state = state;
    
                    var token = changeTokenProducer();
    
                    RegisterChangeTokenCallback(token);
                }
    
                /// 1.先执行消费者动作,再绑定新的token,防止消费者执行并发动作
                /// 2.否则的话可能出现以下情况:如果在绑定新token的回调方法后,并且在执行callback之前,新token的状态发生了改变,此时第二次callback也会执行,这样会造成callback的并发执行。
                private void OnChangeTokenFired()
                {
                    var token = _changeTokenProducer();
    
                    try
                    {
                        _changeTokenConsumer(_state);
                    }
                    finally
                    {
                        RegisterChangeTokenCallback(token);
                    }
                }
    
                private void RegisterChangeTokenCallback(IChangeToken token)
                {
                    var registraton = token.RegisterChangeCallback(s => ((ChangeTokenRegistration<TState>)s).OnChangeTokenFired(), this);
                    SetDisposable(registraton);
                }
    
                /// 1.将当前使用的IChangeToken保存到_disposable字段中
                /// 2.如果本对象已经释放,立刻释放新产生的 IChangeToken
                /// 3.已经失效的 IChangeToken 由于已经不被引用,等待GC自动释放(为什么不手动释放?)
                private void SetDisposable(IDisposable disposable)
                {
                    // 读取当前保存的 IChangeToken
                    var current = Volatile.Read(ref _disposable);
    
                    // 如果本对象已经释放,立刻释放新产生的IChangeToken
                    if (current == _disposedSentinel)
                    {
                        disposable.Dispose();
                        return;
                    }
    
                    // 否则更新_disposable字段,返回原值
                    var previous = Interlocked.CompareExchange(ref _disposable, disposable, current);
    
                    // current = 之前的 IChangeToken
                    
                    if (previous == _disposedSentinel)
                    {
                        // 更新失败 说明对象已释放 previous = _disposedSentinel
                        // 本对象已经释放,立刻释放新产生的IChangeToken
                        disposable.Dispose();
                    }
                    else if (previous == current)
                    {
                        // 更新成功 previous 是之前的 IChangeToken
                    }
                    else
                    {
                        // 如果其他人为 _disposable赋值,且值不为 _disposedSentinel
                        // 会造成对象未释放、更新失败的情况
                        throw new InvalidOperationException("Somebody else set the _disposable field");
                    }
                }
    
                // 释放当前保存的 IChangeToken,将字段赋值为_disposedSentinel
                public void Dispose()
                {
                    Interlocked.Exchange(ref _disposable, _disposedSentinel).Dispose();
                }
    
                private class NoopDisposable : IDisposable
                {
                    public void Dispose()
                    {
                    }
                }
            }
        }
    

    4.6 ChangeToken.OnChange 测试方法

    该测试方法通过一个ChangeTokenProducer类来模拟内部的状态改变。通过内部维护一个ConfigurationReloadToken,可以向外部发出更改通知(一次性)。为了实现向外部持续发出更改通知,可以在更改ConfigurationReloadToken状态之前,重新实例化有一个新的IChangeToken,供外部重新绑定回调方法。

        class ChangeTokenTest
        {
            public static void Run() {
                var ctsProducer = new ChangeTokenProducer();
                var subscriber = ChangeToken.OnChange(() => ctsProducer.GetReloadToken(), () =>
                {
                    Console.WriteLine("消费者观察到改变事件");
                });
                Console.ReadLine();
            }
    
            /// <summary>
            /// 假设该类需要在内部状态发生改变时向外界发送更改通知
            /// </summary>
            private class ChangeTokenProducer
            {
                // cts只能执行一次相应动作
                private ConfigurationReloadToken _changetoken = new ConfigurationReloadToken();
    
                /// <summary>
                /// 模拟状态改变
                /// </summary>
                public ChangeTokenProducer()
                {
                    Task.Run(()=> {
                        while (true)
                        {
                            Thread.Sleep(3000);//模拟耗时
                            //内部状态发生改变,通知外部
                            RaiseChanged();
                        }
                    });
                }
    
                public IChangeToken GetReloadToken () => _changetoken;
    
                private void RaiseChanged() {
                    //产生新的cts
                    var previousToken = Interlocked.Exchange(ref _changetoken, new ConfigurationReloadToken());
                    //触发老的cts动作
                    //外界执行响应动作时,通过GetReloadToken()获取新的cts,执行相应动作,并重新绑定回调函数
                    previousToken.OnReload();
                }
            }
        }
    

    5. IConfigurationSource 的实现

    IConfigurationSource拥有一个实现IFileProvider接口的类属性。默认实现为PhysicalFileProvider类,文件监控目录默认为程序集根目录。该类提供文件的访问和监控功能。在Build方法中实例化JsonFileConfigurationProvider,并将自身传递进去。
    在.NetCore源码中JsonConfigurationSource 是继承 抽象类FileConfigurationSource 的。此处合并了两个类的代码。

        public class JsonFileConfigurationSource : IConfigurationSource
        {
            public IFileProvider FileProvider { get; set; }
    
            public IConfigurationProvider Build(IConfigurationBuilder builder) {
                EnsureDefaults(builder);
                return new JsonFileConfigurationProvider(this);
            }
    
            public void EnsureDefaults(IConfigurationBuilder builder)
            {
                FileProvider = FileProvider ?? builder.GetFileProvider();
            }
        }
        
        public static class FileConfigurationExtensions 
        {
            /// 获取默认的IFileProvider,root目录默认为程序集根目录(AppContext.BaseDirectory)
            public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
            {
                if (builder == null)
                {
                    throw new ArgumentNullException(nameof(builder));
                }
                return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
            }
        }
    

    6. IConfigurationProvider 的实现

    在Core的源码中继承关系为JsonConfigurationProvider: FileConfigurationProvider:ConfigurationProvider:IConfigurationProvider
    。本项目代码合并了JsonConfigurationProvider FileConfigurationProvider这两个类

    6.1 ConfigurationProvider 的实现

    该类使用一个字典用于保存配置项的字符串键值对。并拥有一个类型为 ConfigurationReloadToken 的字段。在配置文件发生更改时,_reloadToken的状态发生改变,外部可以通过观察该字段的状态来得知配置文件发生更改。

        /// <summary>
        /// 配置提供者抽象类
        /// </summary>
        public abstract class ConfigurationProvider : IConfigurationProvider
        {
            private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
    
            /// 初始化存储配置的字典,键值忽略大小写
            protected ConfigurationProvider()
            {
                Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            }
            
            /// 存储配置的键值对,protected只能在子类中访问
            protected IDictionary<string, string> Data { get; set; }
    
            /// 读取键值
            public virtual bool TryGet(string key, out string value) => Data.TryGetValue(key, out value);
    
            /// 设置键值
            public virtual void Set(string key, string value) => Data[key] = value;
    
            /// 加载配置数据源,使用virtual修饰符,在子类中实现重写
            public virtual void Load()
            { }
    
            public IChangeToken GetReloadToken()
            {
                return _reloadToken;
            }
    
            /// <summary>
            /// 触发change token,并生成一个新的change token
            /// </summary>
            protected void OnReload()
            {
                //原子操作:赋值并返回原始值
                var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
                previousToken.OnReload();
            }
        }
    

    6.2 JsonFileConfigurationProvider 的实现

    该类继承于抽象类ConfigurationProvider
    在构造函数中监听 FileProvider.Watch() 方法返回的IChangeToken,收到更改通知时执行以下两个动作,一个是重新读取文件流,加载到字典中;另一个是改变_reloadToken 的状态,用于通知外部:已经重新加载配置文件。由于在本项目中直接引用了MS的PhysicalFileProvider,而该类监听文件返回的是微软的IChangeToken。为了兼容项目代码,通过一个适配类来转换接口。

        public class JsonFileConfigurationProvider : ConfigurationProvider, IDisposable
        {
            private readonly IDisposable _changeTokenRegistration;
            
            public JsonFileConfigurationSource Source { get; }
    
            public JsonFileConfigurationProvider(JsonFileConfigurationSource source)
            {
                if (source == null)
                {
                    throw new ArgumentNullException(nameof(source));
                }
                Source = source;
    
                if (Source.ReloadOnChange && Source.FileProvider != null)
                {
                    //1.IFileProvider.Watch(string filter) 返回IChangeToken
                    //2.绑定IChangeToken的回调函数(1。生成新的IChangeToken 2.读取配置文件、向ConfigurationRoot传递消息)
                    //3.检测到文件更改时,触发回调
                    //4.为新的IChangeToken绑定回调函数
                    _changeTokenRegistration = ChangeToken.OnChange(
                        () => new IChangeTokenAdapter(Source.FileProvider.Watch(Source.Path)),
                        () => {
                            Thread.Sleep(Source.ReloadDelay);
                            Load(reload: true);
                        });
                }
            }
    
            //重新加载文件并向IConfigurationRoot传递更改通知
            private void Load(bool reload)
            {
                var file = Source.FileProvider?.GetFileInfo(Source.Path);
                if (file == null || !file.Exists)
                {
                    if (Source.Optional || reload) // Always optional on reload
                    {
                        Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                    }
                    else
                    {
                        //处理异常
                    }
                }
                else
                {
                    // Always create new Data on reload to drop old keys
                    if (reload)
                    {
                        Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                    }
                    using (var stream = file.CreateReadStream())
                    {
                        try
                        {
                            Load(stream);
                        }
                        catch (Exception e)
                        {
                            HandleException(new FileLoadExceptionContext() { Exception = e, Provider = this, Ignore = true });
                        }
                    }
                }
                //触发IConfigurationProvider._cts.Cancel(),向IConfigurationRoot传递更改通知
                OnReload();
            }
    
            public override void Load()
            {
                Load(reload: false);
            }
    
            /// 从文件流中加载数据到IConfigurationProvider的Data中
            public void Load(Stream stream) {
                try
                {
                    //.NetCore3.0使用JsonDocument读取json文件,生成结构化文档:
                    /// [key] 节点1-1:节点2-1:节点3-1 [value] Value1
                    /// [key] 节点1-1:节点2-2:节点3-2 [value] Value2
                    /// Data = JsonConfigurationFileParser.Parse(stream);
                    //此处使用Newtonsoft.Json,简单的序列化为普通键值对
                    using (StreamReader sr = new StreamReader(stream))
                    {
                        String jsonStr = sr.ReadToEnd();
                        Data = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(jsonStr);
                    }
                }
                catch (Exception e)
                {
                    throw new FormatException("读取文件流失败", e);
                }
            }
    
            public void Dispose() => Dispose(true);
    
            /// 释放_changeTokenRegistration
            protected virtual void Dispose(bool disposing)
            {
                _changeTokenRegistration?.Dispose();
            }
        }
    
        /// 适配器类 
        /// 将Microsoft.Extensions.Primitives.IChangeToken转换为CoreWebApp.Primitives.IChangeToken
        public class IChangeTokenAdapter : IChangeToken
        {
            public IChangeTokenAdapter(IChangeTokenMS msToken)
            {
                MsToken = msToken ?? throw new ArgumentNullException(nameof(msToken));
            }
    
            private IChangeTokenMS MsToken { get; set; }
    
            public bool HasChanged => MsToken.HasChanged;
    
            public bool ActiveChangeCallbacks => MsToken.ActiveChangeCallbacks;
    
            public IDisposable RegisterChangeCallback(Action<object> callback, object state)
            {
                return MsToken.RegisterChangeCallback(callback, state);
            }
        }
    

    7. ConfigurationSection 类的实现

    {
      "OptionV1": {
        "OptionV21": "ValueV21",
        "OptionV22": {
          "OptionV31": "ValueV31",
          "OptionV32": "ValueV32"
        }
      }
    }
    

    对于如上的配置文件会保存为key "OptionV1:OptionV22:OptionV31" value "ValueV31"的格式,这样同时将节点间的层级关系也保存了下来。通过ConfigurationRoot访问键值需要提供键的全路径。ConfigurationSection 类相当于定位了某个节点,通过ConfigurationSection访问键值只需要通过相对路径。


    结语

    到此为止,ConfigurationRoot已经构建完成,然后通过DI模块以单例模式注入到系统中。在控制器中可以通过IConfiguration访问到所有配置源的键值对,并且当配置文件发生改变时重新加载IConfigurationProvider。下篇文章将会讲述从如何通过强类型IOptions 访问配置项。

  • 相关阅读:
    如何在markdown隐藏代码块
    html基础
    驻留机制
    字典
    echarts简单使用
    selenium的基本操作
    Excel上传、下载、models 自定义字段、批量执行(可选)
    django之自定义标签(路径url反向解码)
    邮件自动生成发送用例报告
    前台获取后台保存的数据
  • 原文地址:https://www.cnblogs.com/Kane-Blake/p/11318366.html
Copyright © 2020-2023  润新知