.NET - 是否可以在同一对象中同时使用 XmlAnyElementAttribute 和 XmlSerializer.UnknownElement 事件

.NET - Is it possible to use both XmlAnyElementAttribute and XmlSerializer.UnknownElement event within the same object

我有一个 class,其中我不得不将 属性 的类型从简单的 List<string> 更改为复杂的 List<CustomObject>

我的问题是,在一段时间内,我会有人使用旧版和新版软件。到目前为止,当我更改合同时,我只是使用 UnknownElement 事件将旧成员映射到新成员,因为它是 private 文件并且它完美地用于向后兼容性但破坏了旧版本,因为它没有写回旧格式。

但这次是共享文件,这让我意识到我错过了向上兼容性,使用旧版本的人会删除新成员。我读到 XmlAnyElementAttribute 以保留未知元素并将它们序列化回文件。这修复了向上兼容性。

我现在已经掌握了拼图的所有部分,但我找不到如何让它们协同工作,因为添加 XmlAnyElementAttribute 似乎以 UnknownElement 未被触发而告终。

我还想简单地回读缺少反序列化事件的 XmlAnyElementAttributeproperty once the deserialization is done but this time, it is theXmlSerializer。

这是两个文件的示例: 旧格式:

<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <ListeCategories>
    <string>SX00</string>
    <string>SX01</string>
  </ListeCategories>
</OptionsSerializable>

新格式:

<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <ListeCategoriesExt>
    <CategoryInfo Name="SX00" Type="Principal" Persistence="Global">
      <ToolTip>SX00</ToolTip>
      <SearchTerm>SX00</SearchTerm>
    </CategoryInfo>
    <CategoryInfo Name="SX01" Type="Principal" Persistence="Global">
      <ToolTip>SX01</ToolTip>
      <SearchTerm>SX01</SearchTerm>
    </CategoryInfo>
  </ListeCategoriesExt>
</OptionsSerializable>

需要:

<OptionsSerializable xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <ListeCategories>
    <string>SX00</string>
    <string>SX01</string>
  </ListeCategories>
  <ListeCategoriesExt>
    <CategoryInfo Name="SX00" Type="Principal" Persistence="Global">
      <ToolTip>SX00</ToolTip>
      <SearchTerm>SX00</SearchTerm>
    </CategoryInfo>
    <CategoryInfo Name="SX01" Type="Principal" Persistence="Global">
      <ToolTip>SX01</ToolTip>
      <SearchTerm>SX01</SearchTerm>
    </CategoryInfo>
  </ListeCategoriesExt>
</OptionsSerializable>

根据 docs:

XmlSerializer.UnknownElement ... Occurs when the XmlSerializer encounters an XML element of unknown type during deserialization.

如果您的 <ListeCategories> 元素绑定到 [XmlAnyElement] 属性,则它们不是未知类型,因此不会引发任何事件。

现在,如果除了 <ListeCategories> 之外还有一些 other 未知元素(未显示在您的问题中),您希望使用 post-process UnknownElement,您可以通过使用 [XmlAnyElementAttribute(string name)]:

限制绑定元素的名称来实现

Initializes a new instance of the XmlAnyElementAttribute class and specifies the XML element name generated in the XML document.

即:

public class OptionsSerializable 
{
    [XmlAnyElement("ListeCategories")]
    public XmlElement [] ListeCategories { get; set; }

现在还有其他未知元素,例如<SomeOtherObsoleteNodeToPostprocess />,仍会引发该事件。演示 fiddle #1 here。但是您仍然不会收到 <ListeCategories>.

的事件回调

那么,您有哪些选择?

首先,您可以在 setter 中对 XmlElement [] 数组进行 post 处理,如 this answer to Better IXmlSerializable format?:

[XmlRoot(ElementName="OptionsSerializable")]
public class OptionsSerializable 
{
    [XmlAnyElement("ListeCategories")]
    public XmlElement [] ListeCategories
    {
        get
        {
            // Convert the ListeCategoriesExt items property to an array of XmlElement
        }
        set
        {
            // Convert array of XmlElement back to ListeCategoriesExt items.
        }
    }

原始的 UnknownElement 事件逻辑也可以通过使用这个来部分保留:

XmlElement[] _unsupported;
[XmlAnyElement()]
 public XmlElement[] Unsupported {
     get {
         return _unsupported;
     }
     set {
         _unsupported = value;
         if ((value.Count > 0)) {
             foreach (element in value) {
                 OnUnknownElementFound(this, new XmlElementEventArgs(){Element=element});
             }
         }
     }
 }

但是,如果 post 处理是由 OptionsSerializable 对象本身完成的,那么将 ListeCategories 视为 ListeCategoriesExt 属性。以下是我的做法:

[XmlRoot(ElementName="OptionsSerializable")]
public class OptionsSerializable 
{
    [XmlArray("ListeCategories"), XmlArrayItem("string")]
    public string [] XmlListeCategories
    {
        //Can't use [Obsolete] because doing so will cause XmlSerializer to not serialize the property, see 
        get
        {
            // Since it seems <CategoryInfo Name="VerifierCoherence" Type="Principal" Persistence="Global"> should not be written back,
            // you will need to add a .Where clause excluding those CategoryInfo items you don't want to appear in the old list of strings.
            return ListeCategoriesExt?.Select(c => c.Name)?.ToArray();
        }
        set
        {
            // Merge in the deserialization results.  Note this algorithm assumes that there are no duplicate names.
            // Convert array of XmlElement back to ListeCategoriesExt items.
            foreach (var name in value)
            {
                if (ListeCategoriesExt.FindIndex(c => c.Name == name) < 0)
                {
                    ListeCategoriesExt.Add(new CategoryInfo
                                           {
                                               Name = name, Type = "Principal", Persistence = "Global",
                                               ToolTip = name,
                                               SearchTerm = name,
                                           });
                }
            }
        }
    }

    [XmlArray("ListeCategoriesExt"), XmlArrayItem("CategoryInfo")]
    public CategoryInfo [] XmlListeCategoriesExt
    {
        get
        {
            return ListeCategoriesExt?.ToArray();
        }
        set
        {
            // Merge in the deserialization results.  Note this algorithm assumes that there are no duplicate names.
            foreach (var category in value)
            {
                var index = ListeCategoriesExt.FindIndex(c => c.Name == category.Name);
                if (index < 0)
                {
                    ListeCategoriesExt.Add(category);
                }
                else
                {
                    // Overwrite the item added during XmlListeCategories deserialization.
                    ListeCategoriesExt[index] = category;
                }
            }
        }
    }

    [XmlIgnore]
    public List<CategoryInfo> ListeCategoriesExt { get; set; } = new List<CategoryInfo>();
}

[XmlRoot(ElementName="CategoryInfo")]
public class CategoryInfo 
{
    [XmlElement(ElementName="ToolTip")]
    public string ToolTip { get; set; }
    [XmlElement(ElementName="SearchTerm")]
    public string SearchTerm { get; set; }
    [XmlAttribute(AttributeName="Name")]
    public string Name { get; set; }
    [XmlAttribute(AttributeName="Type")]
    public string Type { get; set; }
    [XmlAttribute(AttributeName="Persistence")]
    public string Persistence { get; set; }
}

备注:

  • 由于<ListeCategories>出现在之前 <ListeCategoriesExt>在您的XML中,需要将新项目合并到XmlListeCategoriesExt setter 中先前反序列化的过时项目。

    如果您要设置 XmlArrayAttribute.Order 两者都需要 <ListeCategories>last.

    [=90,则没有必要这样做=]
  • 由于合并的需要,反序列化算法不支持多个CategoryInfo同名对象。

    如果您的 CategoryInfo 列表中必须有相同的名称,则合并新旧表示会变得更加复杂。

  • 不幸的是,无法在 OnDeserialized 事件中合并新旧类别列表,因为令人讨厌的是,XmlSerializer does not support [OnDeserialized].

演示 fiddle #2 here.