YamlDotNet如何反序列化Sequence或Mapping类型的配置?
问题描述
我的应用需要加载YAML配置文件,原配置中sonarr是数组(Sequence)形式:
sonarr: - base_url: abc1 api_key: xyz1 - base_url: abc2 api_key: xyz2
为支持命名实例,我想将其改为映射(Mapping)形式,同时兼容旧的数组形式(并给出弃用提示),新配置如下:
sonarr: instance1: base_url: abc1 api_key: xyz1 instance2: base_url: abc2 api_key: xyz2
我尝试了以下C#代码,但无法正常工作:
public IEnumerable<T> LoadFromStream(TextReader stream, string configSection) { var parser = new Parser(stream); parser.Consume<StreamStart>(); parser.Consume<DocumentStart>(); parser.Consume<MappingStart>(); var validConfigs = new List<T>(); while (parser.TryConsume<Scalar>(out var key)) { if (key.Value != configSection) { parser.SkipThisAndNestedEvents(); continue; } var evt = parser.Consume<NodeEvent>(); var configs = evt switch { SequenceStart => _deserializer.Deserialize<Dictionary<string, T>>(parser) .Select(kvp => { kvp.Value.Name = kvp.Key; return kvp.Value; }) .ToList(), MappingStart => _deserializer.Deserialize<List<T>>(parser), _ => null }; if (configs is not null) { ValidateConfigs(configSection, configs, validConfigs); } parser.SkipThisAndNestedEvents(); } return validConfigs; }
问题在于Consume和TryConsume会消耗MappingStart/SequenceStart节点,导致后续无法用List<T>或Dictionary<string, T>进行反序列化。我需要类似“peek”的功能判断节点类型,再做对应反序列化,请问该如何处理这种场景,或者有没有更通用的灵活Schema方案?
解决方案
1. 用Parser的Peek功能判断节点类型
YAML.NET的Parser类提供Peek方法,可查看下一个事件而不消耗它。你可以先用Peek判断sonarr对应的节点类型,再选择反序列化方式,注意不要提前消耗关键节点:
public IEnumerable<T> LoadFromStream(TextReader stream, string configSection) { var parser = new Parser(stream); parser.Consume<StreamStart>(); parser.Consume<DocumentStart>(); parser.Consume<MappingStart>(); var validConfigs = new List<T>(); while (parser.TryConsume<Scalar>(out var key)) { if (key.Value != configSection) { parser.SkipThisAndNestedEvents(); continue; } // 先Peek下一个事件,判断类型 var nextEvent = parser.Peek(); List<T> configs = null; if (nextEvent is SequenceStart) { // 处理旧数组格式,给出弃用提示 configs = _deserializer.Deserialize<List<T>>(parser); Console.WriteLine($"警告:{configSection}的数组格式已弃用,请使用命名实例的映射格式。"); // 给旧实例设置默认名称 for (int i = 0; i < configs.Count; i++) { configs[i].Name = $"instance{i+1}"; } } else if (nextEvent is MappingStart) { // 处理新映射格式 var instanceDict = _deserializer.Deserialize<Dictionary<string, T>>(parser); configs = instanceDict.Select(kvp => { kvp.Value.Name = kvp.Key; return kvp.Value; }).ToList(); } if (configs is not null) { ValidateConfigs(configSection, configs, validConfigs); } } return validConfigs; }
2. 自定义类型转换器兼容两种格式
创建IYamlTypeConverter实现类,统一处理两种格式的反序列化,更符合YAML.NET的扩展机制:
步骤1:实现自定义转换器
public class ConfigListConverter<T> : IYamlTypeConverter where T : class, new() { private readonly IDeserializer _deserializer; private readonly string _configSectionName; public ConfigListConverter(IDeserializer deserializer, string configSectionName) { _deserializer = deserializer; _configSectionName = configSectionName; } public bool Accepts(Type type) { return type == typeof(List<T>); } public object ReadYaml(IParser parser, Type type) { var configs = new List<T>(); var nextEvent = parser.Peek(); if (nextEvent is SequenceStart) { // 解析旧数组格式 parser.Consume<SequenceStart>(); while (parser.TryConsume<MappingStart>(out _)) { var config = _deserializer.Deserialize<T>(parser); configs.Add(config); parser.Consume<MappingEnd>(); } parser.Consume<SequenceEnd>(); Console.WriteLine($"警告:{_configSectionName}的数组格式已弃用,请使用命名实例格式。"); // 设置默认名称 for (int i = 0; i < configs.Count; i++) { var prop = typeof(T).GetProperty("Name"); prop?.SetValue(configs[i], $"instance{i+1}"); } } else if (nextEvent is MappingStart) { // 解析新映射格式 parser.Consume<MappingStart>(); while (parser.TryConsume<Scalar>(out var instanceName)) { var config = _deserializer.Deserialize<T>(parser); var prop = typeof(T).GetProperty("Name"); prop?.SetValue(config, instanceName.Value); configs.Add(config); } parser.Consume<MappingEnd>(); } return configs; } public void WriteYaml(IEmitter emitter, object value, Type type) { // 序列化时输出新映射格式 var configs = (List<T>)value; emitter.Emit(new MappingStart()); foreach (var config in configs) { var prop = typeof(T).GetProperty("Name"); var name = prop?.GetValue(config)?.ToString() ?? $"instance{configs.IndexOf(config)+1}"; emitter.Emit(new Scalar(name)); _deserializer.Serialize(emitter, config); } emitter.Emit(new MappingEnd()); } }
步骤2:注册转换器并使用
// 初始化反序列化器时注册转换器 var deserializer = new DeserializerBuilder() .WithConverter(new ConfigListConverter<SonarrConfig>(_deserializer, "sonarr")) .Build(); // 直接反序列化整个配置类 var appConfig = deserializer.Deserialize<AppConfig>(stream); // AppConfig类定义示例: // public class AppConfig { public List<SonarrConfig> Sonarr { get; set; } }
3. 关键注意事项
- 不要提前消耗
SequenceStart/MappingStart节点,确保反序列化器能完整读取节点内容。 - 处理旧格式必须添加弃用提示,引导用户迁移到新格式。
- 自定义转换器要正确处理节点的开始和结束事件,避免解析异常。
内容的提问来源于stack exchange,提问作者void.pointer
相关产品推荐
相关产品推荐

