6

我正在尝试使用YamlDotNet解析此 YAML 文档:

title: Document with dynamic properties
/a:
  description: something
/b:
  description: other something

进入这个对象:

public class SimpleDoc
{

    [YamlMember(Alias = "title")]
    public string Title { get; set;}

    public Dictionary<string, Path> Paths { get; set; }
}

public class Path
{
    [YamlMember(Alias = "description")]
    public string Description {get;set;}
}

我希望 /a /b 或任何其他不匹配的属性最终出现在 Paths 字典中。如何配置YamlDotNet以支持这种情况?

反序列化设置是:

// file paths is the path to the specified doc :)
var deserializer = new Deserializer();
var doc = deserializer.Deserialize<SimpleDoc>(File.OpenText(filePath));

但是,鉴于SimpleDoc上没有属性/a ,它会失败。如果我在构造函数中将设置ignoreUnmatched设置为 true,它会按预期被忽略。

4

1 回答 1

0

我需要一个项目,其中 YAML 模式被定义为 JSON 模式,我用它来生成类(通过NJsonSchema)。但是,实际数据以 YAML 形式出现。所以,我需要将 YAML 反序列化到类中。

我利用这种机制,如果一个类实现IDictionary<string, objector IDictionary<object, object>YamlDotNet会将来自该节点的所有键/值放入字典中。如果我从 派生类Dictionary<string, object>,我将无权修改将值添加到该字典的方式,因此我被迫实现该接口。

internal partial class Language : IDictionary<string, object>
{
    [YamlMember(Alias = "namespace")]
    public string Namespace { get; set; }

    [YamlMember(Alias = "discriminatorValue")]
    public string DiscriminatorValue { get; set; }

    [YamlMember(Alias = "uid")]
    public string Uid { get; set; }

    [YamlMember(Alias = "internal")]
    public bool Internal { get; set; }

    [YamlIgnore]
    public IDictionary<string, object> AdditionalProperties = new Dictionary<string, object>();

    private readonly Dictionary<string, object> _dictionary = new Dictionary<string, object>();
    private static readonly Dictionary<string, PropertyInfo> DeserializableProperties = typeof(Language).GetDeserializableProperties();

    // Workaround for mapping properties from the dictionary entries
    private void AddAndMap(string key, object value)
    {
        _dictionary.Add(key, value);

        if (DeserializableProperties.ContainsKey(key))
        {
            var propInfo = DeserializableProperties[key];
            propInfo.SetValue(this, propInfo.DeserializeDictionary(value));
            return;
        }

        AdditionalProperties.Add(key, value);
    }

    public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _dictionary.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public void Add(KeyValuePair<string, object> item) => AddAndMap(item.Key, item.Value);
    public void Clear() => _dictionary.Clear();
    public bool Contains(KeyValuePair<string, object> item) => _dictionary.ContainsKey(item.Key);
    public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
    {
        foreach (var item in _dictionary)
        {
            array[arrayIndex++] = item;
        }
    }
    public bool Remove(KeyValuePair<string, object> item) => _dictionary.Remove(item.Key);

    public int Count => _dictionary.Count;
    public bool IsReadOnly => false;
    public void Add(string key, object value) => AddAndMap(key, value);
    public bool ContainsKey(string key) => _dictionary.ContainsKey(key);
    public bool Remove(string key) => _dictionary.Remove(key);
    public bool TryGetValue(string key, out object value) => _dictionary.TryGetValue(key, out value);

    public object this[string key]
    {
        get => _dictionary[key];
        set => AddAndMap(key, value);
    }

    public ICollection<string> Keys => _dictionary.Keys;
    public ICollection<object> Values => _dictionary.Values;
}

在这个例子中,Language是我试图反序列化的类。作为我生成的类的一部分(通过 JSON 模式生成器),我在这个类上有其他属性。由于它们是部分的,因此我可以实施此解决方法。

基本上,我创建AddAndMap并将其应用到界面中添加字典条目的任何位置。我还创建了AdditionalProperties字典来保存我们的价值观。请记住,我们需要IDictionary正确序列化此类的所有值。

接下来,我总是将条目添加到支持中_dictionary,然后确定我是否有一个属性来提供值。如果我没有该值应映射到的属性,则将值放入AdditionalProperties字典中。

为了确定我是否有可用于反序列化的属性,我必须编写一些扩展方法。

public static Dictionary<string, PropertyInfo> GetDeserializableProperties(this Type type) => type.GetProperties()
    .Select(p => new KeyValuePair<string, PropertyInfo>(p.GetCustomAttributes<YamlMemberAttribute>(true).Select(yma => yma.Alias).FirstOrDefault(), p))
    .Where(pa => !pa.Key.IsNullOrEmpty()).ToDictionary(pa => pa.Key, pa => pa.Value);

// Only allows deserialization of properties that are primitives or type Dictionary<object, object>. Does not support properties that are custom classes.
public static object DeserializeDictionary(this PropertyInfo info, object value)
{
    if (!(value is Dictionary<object, object>)) return TypeConverter.ChangeType(value, info.PropertyType);

    var type = info.PropertyType;
    var properties = type.GetDeserializableProperties();
    var property = Activator.CreateInstance(type);
    var matchedProperties = ((Dictionary<object, object>)value).Where(e => properties.ContainsKey(e.Key.ToString()));
    foreach (var (propKey, propValue) in matchedProperties)
    {
        var innerInfo = properties[propKey.ToString()];
        innerInfo.SetValue(property, innerInfo.DeserializeDictionary(propValue));
    }
    return property;
}

需要注意的主要事情是,这仅适用于基元类的属性或Dictionary<object, object>. 原因是因为它用于TypeConverter.ChangeType在设置属性时返回适当的对象。如果您设法解释如何创建自定义类,您只需将TypeConverter.ChangeType调用替换为该解决方案。

为了获取类的可反序列化属性,我使用反射来获取YamlMemberAttribute该类的属性。然后,我PropertyInfo将该属性的 放入字典中,其中键是 的AliasYamlMemberAttribute。在我的解决方案中,所有属性都需要为 property 定义一个别名

然后,我找到具有与我从AddAndMap调用中获得的密钥匹配的别名的属性。这会递归地发生,因此(将来)可以使用自定义类属性进行这项工作。Dictionary<object, object>它作为值类型进行检查,因为YamlDotNet在将某些内容反序列化为字典时,总是将子类反序列化为Dictionary<object, object>.

全面的

如果您尝试反序列化的类只有平面(没有自定义类属性),则此解决方案有效并且可以简化。或者,它可以扩展为支持自定义类属性。该解决方案也可以制成其他类派生自的基本类型,并且您可以GetType()使用 inDeserializableProperties而不是typeof(Language). 这是一个过于冗长的解决方案,但在YamlDotNet有一个更清洁/更简单/适当的解决方案之前,这是我在探索他们的源代码后能想到的最好的解决方案。

最后,请记住,由于您的类派生自IDictionaryYamlDotNet 因此不会序列化您的类中的任何属性,即使它们具有YamlMember属性。如果YamlDotNet可以提供该功能,那么我最初就想到了一个更清洁的解决方案。唉,事实并非如此。

于 2019-10-04T20:29:26.557 回答