YamlFileCfgSource.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. using Apq.Cfg.Sources;
  2. using Apq.Cfg.Sources.File;
  3. using Microsoft.Extensions.Configuration;
  4. using YamlDotNet.RepresentationModel;
  5. namespace Apq.Cfg.Yaml;
  6. /// <summary>
  7. /// YAML 文件配置源,支持读取和写入 YAML 格式的配置文件
  8. /// </summary>
  9. internal sealed class YamlFileCfgSource : FileCfgSourceBase, IWritableCfgSource
  10. {
  11. /// <summary>
  12. /// 初始化 YamlFileCfgSource 实例
  13. /// </summary>
  14. /// <param name="path">YAML 文件路径</param>
  15. /// <param name="level">配置层级,数值越大优先级越高</param>
  16. /// <param name="writeable">是否可写</param>
  17. /// <param name="optional">是否为可选文件</param>
  18. /// <param name="reloadOnChange">文件变更时是否自动重载</param>
  19. /// <param name="isPrimaryWriter">是否为主要写入源</param>
  20. public YamlFileCfgSource(string path, int level, bool writeable, bool optional, bool reloadOnChange,
  21. bool isPrimaryWriter)
  22. : base(path, level, writeable, optional, reloadOnChange, isPrimaryWriter)
  23. {
  24. }
  25. /// <summary>
  26. /// 构建 Microsoft.Extensions.Configuration 的 YAML 配置源
  27. /// </summary>
  28. /// <returns>YamlSource 实例,内部实现了 IConfigurationSource</returns>
  29. public override IConfigurationSource BuildSource()
  30. {
  31. var (fp, file) = CreatePhysicalFileProvider(_path);
  32. var src = new YamlSource
  33. {
  34. FileProvider = fp,
  35. Path = file,
  36. Optional = _optional,
  37. ReloadOnChange = _reloadOnChange
  38. };
  39. src.ResolveFileProvider();
  40. return src;
  41. }
  42. /// <summary>
  43. /// 应用配置更改到 YAML 文件
  44. /// </summary>
  45. /// <param name="changes">要应用的配置更改</param>
  46. /// <param name="cancellationToken">取消令牌</param>
  47. /// <returns>表示异步操作的任务</returns>
  48. /// <exception cref="InvalidOperationException">当配置源不可写时抛出</exception>
  49. public async Task ApplyChangesAsync(IReadOnlyDictionary<string, string?> changes, CancellationToken cancellationToken)
  50. {
  51. if (!IsWriteable)
  52. throw new InvalidOperationException($"配置源 (层级 {Level}) 不可写");
  53. EnsureDirectoryFor(_path);
  54. var yaml = new YamlStream();
  55. if (File.Exists(_path))
  56. {
  57. var readEncoding = DetectEncodingEnhanced(_path);
  58. using var sr = new StreamReader(_path, readEncoding, true);
  59. yaml.Load(sr);
  60. }
  61. else
  62. {
  63. yaml.Add(new YamlDocument(new YamlMappingNode()));
  64. }
  65. var root = yaml.Documents.Count > 0 ? (YamlMappingNode)yaml.Documents[0].RootNode : new YamlMappingNode();
  66. foreach (var (key, val) in changes)
  67. SetYamlByColonKey(root, key, val);
  68. using var writer = new StreamWriter(_path, false, GetWriteEncoding());
  69. yaml.Save(writer, false);
  70. await writer.FlushAsync().ConfigureAwait(false);
  71. }
  72. /// <summary>
  73. /// 根据冒号分隔的键路径设置 YAML 映射节点中的值
  74. /// </summary>
  75. /// <param name="root">YAML 映射根节点</param>
  76. /// <param name="key">冒号分隔的键路径(如 "Database:Connection:Timeout")</param>
  77. /// <param name="value">要设置的值,为 null 时设置为空字符串</param>
  78. private static void SetYamlByColonKey(YamlMappingNode root, string key, string? value)
  79. {
  80. var parts = key.Split(':', StringSplitOptions.RemoveEmptyEntries);
  81. var current = root;
  82. for (var i = 0; i < parts.Length; i++)
  83. {
  84. var part = parts[i];
  85. if (i == parts.Length - 1)
  86. {
  87. current.Children[new YamlScalarNode(part)] = new YamlScalarNode(value ?? string.Empty);
  88. }
  89. else
  90. {
  91. if (!current.Children.TryGetValue(new YamlScalarNode(part), out var child) ||
  92. child is not YamlMappingNode childMap)
  93. {
  94. childMap = new YamlMappingNode();
  95. current.Children[new YamlScalarNode(part)] = childMap;
  96. }
  97. current = childMap;
  98. }
  99. }
  100. }
  101. private sealed class YamlSource : FileConfigurationSource
  102. {
  103. public override IConfigurationProvider Build(IConfigurationBuilder builder)
  104. {
  105. EnsureDefaults(builder);
  106. return new YamlProvider(this);
  107. }
  108. }
  109. private sealed class YamlProvider : FileConfigurationProvider
  110. {
  111. public YamlProvider(YamlSource source) : base(source) { }
  112. public override void Load(Stream stream)
  113. {
  114. var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
  115. using var reader = new StreamReader(stream, System.Text.Encoding.UTF8, true);
  116. var yaml = new YamlStream();
  117. yaml.Load(reader);
  118. if (yaml.Documents.Count == 0)
  119. {
  120. Data = data;
  121. return;
  122. }
  123. var root = (YamlMappingNode)yaml.Documents[0].RootNode;
  124. VisitNode(root, null, data);
  125. Data = data;
  126. }
  127. private static void VisitNode(YamlNode node, string? prefix, IDictionary<string, string?> data)
  128. {
  129. switch (node)
  130. {
  131. case YamlMappingNode map:
  132. foreach (var kv in map.Children)
  133. VisitNode(kv.Value, CombineKey(prefix, kv.Key.ToString()), data);
  134. break;
  135. case YamlSequenceNode seq:
  136. var idx = 0;
  137. foreach (var item in seq.Children)
  138. VisitNode(item, CombineKey(prefix, (idx++).ToString()), data);
  139. break;
  140. default:
  141. data[prefix ?? string.Empty] = node.ToString();
  142. break;
  143. }
  144. }
  145. private static string CombineKey(string? prefix, string key)
  146. => string.IsNullOrEmpty(prefix) ? key : prefix + ":" + key;
  147. }
  148. }