ASP.NET Core : 二十三. 深入聊一聊配置的内部处理机制(三)

二、数据源的加载

从图18‑5可知,所有类型数据源最终创建的XXXConfigurationProvider都继承自ConfigurationProvider,所以它们都有一个Load方法和一个IDictionary<string, string> 类型的Data 属性,它们是整个配置系统的重要核心。Load方法用于数据源的数据的读取与处理,而Data用于保存最终结果。通过逐一调用Provider的Load方法完成了整个配置系统的数据加载。


以JsonConfigurationProvider为例,它继承自FileConfigurationProvider,所以先看一下FileConfigurationProvider的代码:

public abstract class FileConfigurationProvider : ConfigurationProvider
{
//省略部分代码
    private void Load(bool reload)
    {
        var file = Source.FileProvider?.GetFileInfo(Source.Path);
        if (file == null || !file.Exists)
        {
        //省略部分代码
        }
        else
        {
            if (reload)
            {
                Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            }
            using (var stream = file.CreateReadStream())
            {
                try
                {
                    Load(stream);
                }
                catch (Exception e)
                {
//省略部分代码
                }
            }
        }
        OnReload();
    }
    public override void Load()
    {
        Load(reload: false);
}
    public abstract void Load(Stream stream);
} 

本段代码的主要功能就是读取文件生成stream,然后调用Load(stream)方法解析文件内容。从图18‑5可知,JsonConfigurationProvider、IniConfigurationProvider、XmlConfigurationProvider都是继承自FileConfigurationProvider,而对应JSON、INI、XML三种数据源来说,只是文件内容的格式不同,所以将通用的读取文件内容的功能交给了FileConfigurationProvider来完成,而这三个子类的ConfigurationProvider只需要将FileConfigurationProvider读取到的文件内容的解析即可。所以这个参数为stream 的Load方法写在JsonConfigurationProvider、IniConfigurationProvider、XmlConfigurationProvider这样的子类中,用于专门处理自身对应的格式的文件。


JsonConfigurationProvider代码如下:

public class JsonConfigurationProvider : FileConfigurationProvider
{
    public JsonConfigurationProvider(JsonConfigurationSource source) : base(source) { }
 
    public override void Load(Stream stream)
    {
        try
        {
            Data = JsonConfigurationFileParser.Parse(stream);
        }
        catch (JsonReaderException e)
        {
            string errorLine = string.Empty;
            if (stream.CanSeek)
            {
                stream.Seek(0, SeekOrigin.Begin);
 
                IEnumerable<string> fileContent;
                using (var streamReader = new StreamReader(stream))
                {
                    fileContent = ReadLines(streamReader);
                    errorLine = RetrieveErrorContext(e, fileContent);
                }
            }
 
            throw new FormatException(Resources.FormatError_JSONParseError(e.LineNumber, errorLine), e);
        }
    }
   //省略部分代码
}

JsonConfigurationProvider中关于JSON文件的解析由JsonConfigurationFileParser.Parse(stream)完成的。最终的解析结果被赋值给了父类ConfigurationProvider的名为Data的属性中。


所以最终每个数据源的内容都分别被解析成了IDictionary<string, string>集合,这个集合作为对应的ConfigurationProvider的一个属性。而众多ConfigurationProvider组成的集合又作为ConfigurationRoot的属性。最终它们的关系图如下图3:


ASP.NET Core : 二十三. 深入聊一聊配置的内部处理机制(三)

图3

到此,配置的加载与数据的转换工作完成。下图4展示了这个过程。

ASP.NET Core : 二十三. 深入聊一聊配置的内部处理机制(三)

图4


三、配置的读取


第一节的例子中,通过_configuration["Theme:Color"]的方式获取到了对应的配置值,这是如何实现的呢?现在我们已经了解了数据源的加载过程,而这个_configuration就是数据源被加载后的最终产出物,即ConfigurationRoot,见图18‑7。它的代码如下:

public class ConfigurationRoot : IConfigurationRoot
{
    private IList<IConfigurationProvider> _providers;
    private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
 
    //省略了上文已讲过的构造方法
 
    public IEnumerable<IConfigurationProvider> Providers => _providers;
    public string this[string key]
    {
        get
        {
            foreach (var provider in _providers.Reverse())
            {
                string value;
 
                if (provider.TryGet(key, out value))
                {
                    return value;
                }
            }
 
            return null;
        }
 
        set
        {
            if (!_providers.Any())
            {
                throw new InvalidOperationException(Resources.Error_NoSources);
            }
 
            foreach (var provider in _providers)
            {
                provider.Set(key, value);
            }
        }
    }
 
    public IEnumerable<IConfigurationSection> GetChildren() => GetChildrenImplementation(null);
 
    internal IEnumerable<IConfigurationSection> GetChildrenImplementation(string path)
    {
        return _providers
            .Aggregate(Enumerable.Empty<string>(),
                (seed, source) => source.GetChildKeys(seed, path))
            .Distinct()
            .Select(key => GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
    }
 
    public IChangeToken GetReloadToken() => _changeToken;
 
    public IConfigurationSection GetSection(string key) 
        => new ConfigurationSection(this, key);
 
    public void Reload()
    {
        foreach (var provider in _providers)
        {
            provider.Load();
        }
        RaiseChanged();
    }
 
    private void RaiseChanged()
    {
        var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }
}

对应_configuration["Theme:Color"]的读取方式的是索引器“string this[string key]”,通过查看其get方法可知,它是通过倒序遍历所有ConfigurationProvider,在ConfigurationProvider的Data中尝试查找是否存在Key为"Theme:Color"的值。这也说明了第一节的例子中,在Theme.json中设置了Theme对象的值后,原本在appsettings.json设置的Theme的值被覆盖的原因。从图18‑6中可以看到,该值其实也是被读取并加载的,只是由于ConfigurationRoot的“倒序”遍历ConfigurationProvider的方式导致后注册的Theme.json中的Theme值先被查找到了。同时验证了所有配置值均认为是string类型的约定。


ConfigurationRoot还有一个GetSection方法,会返回一个IConfigurationSection对象,对应的是ConfigurationSection类。它的代码如下:

public class ConfigurationSection : IConfigurationSection
    {
        private readonly ConfigurationRoot _root;
        private readonly string _path;
        private string _key;
 
        public ConfigurationSection(ConfigurationRoot root, string path)
        {
            if (root == null)
            {
                throw new ArgumentNullException(nameof(root));
            }
 
            if (path == null)
            {
                throw new ArgumentNullException(nameof(path));
            }
 
            _root = root;
            _path = path;
        }
 
        public string Path => _path;
        public string Key
        {
            get
            {
                if (_key == null)
                {
                    // Key is calculated lazily as last portion of Path
                    _key = ConfigurationPath.GetSectionKey(_path);
                }
                return _key;
            }
        }
        public string Value
        {
            get
            {
                return _root[Path];
            }
            set
            {
                _root[Path] = value;
            }
        }
        public string this[string key]
        {
            get
            {
                return _root[ConfigurationPath.Combine(Path, key)];
            }
 
            set
            {
                _root[ConfigurationPath.Combine(Path, key)] = value;
            }
        }
 
        public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
 
        public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
 
        public IChangeToken GetReloadToken() => _root.GetReloadToken();
}

它的代码很简单,可以说没有什么实质的代码,它只是保存了当前路径和对ConfigurationRoot的引用。它的方法大多是通过调用ConfigurationRoot的对应方法完成的,通过它自身的路径计算在ConfigurationRoot中对应的Key,从而获取对应的值。而ConfigurationRoot对配置值的读取功能以及数据源的重新加载功能(Reload方法)也是通过ConfigurationProvider实现的,实际数据也是保存在ConfigurationProvider的Data值中。所以ConfigurationRoot和ConfigurationSection就像一个外壳,自身并不负责数据源的加载(或重载)与存储,只负责构建了一个配置值的读取功能。


上一篇:ASP.NET Core : 二十三. 深入聊一聊配置的内部处理机制(一)


下一篇:windows自定义资源 - windows 2003