二、数据源的加载
从图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:
图3
到此,配置的加载与数据的转换工作完成。下图4展示了这个过程。
图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就像一个外壳,自身并不负责数据源的加载(或重载)与存储,只负责构建了一个配置值的读取功能。