【问题标题】:XMLSerializer warns about unknown nodes/attributes when deserializing derived typesXMLSerializer 在反序列化派生类型时警告未知节点/属性
【发布时间】:2017-02-20 11:04:50
【问题描述】:

我最近使用 XMLSerializer 注册了未知节点、元素和属性的事件处理程序,用于从类型层次结构中反序列化复杂类型。我这样做是因为我收到的一些 XML 来自第三方;我对可能给我带来麻烦的数据格式更改感兴趣。

在 XMLSerializer 生成的 XML 中,它使用标准 XML 属性 xsi:type="somederivedtypename" 来识别由 XML 元素表示的实际派生类型。

我惊讶地发现,同一个序列化程序在反序列化时将它刚刚产生的相同属性视为未知。不过有趣的是,反序列化是正确且完整的(在我的实际程序中也有更复杂的类型和数据)。这意味着序列化程序在反序列化的早期阶段会正确评估类型信息。但是在后来的数据提取阶段,该属性显然被误认为是对象的真实数据部分,这当然是未知的。

在我的应用程序中,无端警告最终会弄乱一个不受欢迎的通用日志文件。在我看来,序列化程序应该读回它生成的 XML,而不会出现问题。我的问题:

  • 我做错了吗?
  • 有解决方法吗?

这里有一个最小的例子:

using System;
using System.IO;
using System.Xml.Serialization;

namespace XsiTypeAnomaly
{
    /// <summary>
    /// A trivial base type.
    /// </summary>
    [XmlInclude(typeof(DerivedT))]
    public class BaseT{}

    /// <summary>
    /// A trivial derived type to demonstrate a serialization issue.
    /// </summary>
    public class DerivedT : BaseT
    {
        public int anInt { get; set; }
    }

    class Program
    {
        private static void serializer_UnknownAttribute
            (   object sender, 
                XmlAttributeEventArgs e )
        {
            Console.Error.WriteLine("Warning: Deserializing " 
                    + e.ObjectBeingDeserialized
                    + ": Unknown attribute "
                    + e.Attr.Name);
                }

        private static void serializer_UnknownNode(object sender, XmlNodeEventArgs e)
        {
            Console.Error.WriteLine("Warning: Deserializing "
                    + e.ObjectBeingDeserialized
                    + ": Unknown node "
                    + e.Name);
        }

        private static void serializer_UnknownElement(object sender, XmlElementEventArgs e)
        {
            Console.Error.WriteLine("Warning: Deserializing "
                    + e.ObjectBeingDeserialized
                    + ": Unknown element "
                    + e.Element.Name);
        }

        /// <summary>
        /// Serialize, display the xml, and deserialize a trivial object.
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            BaseT aTypeObj = new DerivedT() { anInt = 1 };
            using (MemoryStream stream = new MemoryStream())
            {
                var serializer = new XmlSerializer(typeof(BaseT));

                // register event handlers for unknown XML bits
                serializer.UnknownAttribute += serializer_UnknownAttribute;
                serializer.UnknownElement += serializer_UnknownElement;
                serializer.UnknownNode += serializer_UnknownNode;

                serializer.Serialize(stream, aTypeObj);
                stream.Flush();

                // output the xml
                stream.Position = 0;
                Console.Write((new StreamReader(stream)).ReadToEnd() + Environment.NewLine);
                stream.Position = 0;
                var serResult = serializer.Deserialize(stream) as DerivedT;

                Console.WriteLine(
                        (serResult.anInt == 1 ? "Successfully " : "Unsuccessfully ")
                    + "read back object");
            }
        }
    }
}

输出:

<?xml version="1.0"?>
<BaseT xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xsi:type="DerivedT">
  <anInt>1</anInt>
</BaseT>
Warning: Deserializing XsiTypeAnomaly.DerivedT: Unknown node xsi:type
Warning: Deserializing XsiTypeAnomaly.DerivedT: Unknown attribute xsi:type
Successfully read back object

【问题讨论】:

  • 警告似乎是有道理的,因为您为BaseT 创建了一个序列化程序,然后实际上输入了一个DerivedT 对象。如果您只是为DerivedT 创建一个序列化程序,警告就会消失。
  • @jstreet 但是这个属性的重点是使基类序列化程序能够反序列化派生对象。想象一个基类列表,它可以包含任何派生类型。调用代码不知道也不关心列表中包含哪些实际派生类型。列表处理代码实际上是在许多派生类型存在之前编写的。
  • 我明白你的意思,并且序列化程序能够反序列化派生对象,但警告的“不便”,因为它实际上不知道属性anInt。建议:当您创建序列化程序时,请使用aTypeObj.GetType(),而不是使用任何显式类型,无论是基类还是派生类。
  • @jstreet 警告与anInt 无关;该元素已正确序列化和反序列化(当我在反序列化后测试非默认值时可以看出)。正如它所说,警告是关于属性xsi:type。属性可以在语法上携带对象信息(即,我可以将anInt 序列化为属性!),但被序列化程序用来存储有关类型的元信息。属性 xmlns:xsixmlns:xsd 被正确识别为“不是对象数据的一部分”,但由于某种原因,xsi:type 不是。我相信这是一个错误。

标签: c# xml xmlserializer xsi xsitype


【解决方案1】:

我是不是做错了什么?

我不这么认为。我同意您的观点,即 XmlSerializer 应该在没有任何警告的情况下反序列化自己的输出。此外,xsi:type 是在XML Schema specification 中定义的标准属性,显然它受 XmlSerializer 支持,如您的示例所示并在MSDN Library 中记录。

因此,这种行为看起来像是一种疏忽。我可以想象一群微软开发人员在开发 .NET Framework 期间致力于 XmlSerializer 的不同方面,并且从未同时测试 xsi:type 和事件。

这意味着序列化程序在反序列化的早期阶段会正确评估类型信息。但是在后来的数据提取阶段,该属性显然被误认为是对象的真实数据部分,这当然是未知的。

你的观察是正确的。

XmlSerializer 类生成一个动态程序集来序列化和反序列化对象。在您的示例中,反序列化 DerivedT 实例的生成方法如下所示:

private DerivedT Read2_DerivedT(bool isNullable, bool checkType)
{
    // [Code that uses isNullable and checkType omitted...]

    DerivedT derivedT = new DerivedT();
    while (this.Reader.MoveToNextAttribute())
    {
        if (!this.IsXmlnsAttribute(this.Reader.Name))
            this.UnknownNode(derivedT);
    }

    this.Reader.MoveToElement();
    // [Code that reads child elements and populates derivedT.anInt omitted...]
    return derivedT;
}

反序列化程序在读取xsi:type 属性并决定创建 DerivedT 的实例后调用此方法。如您所见,while 循环为除xmlns 属性之外的所有属性引发 UnknownNode 事件。这就是为什么您会收到 xsi:type 的 UnknownNode(和 UnknownAttribute)事件。

while 循环由内部XmlSerializationReaderILGen.WriteAttributes 方法生成。代码相当复杂,但我没有看到会导致 xsi:type 属性被跳过的代码路径(除了我在下面描述的第二种解决方法)。

有解决办法吗?

我会忽略 xsi:type 的 UnknownNode 和 UnknownAttribute 事件:

private static void serializer_UnknownNode(object sender, XmlNodeEventArgs e)
{
    if (e.NodeType == XmlNodeType.Attribute &&
        e.NamespaceURI == XmlSchema.InstanceNamespace && e.LocalName == "type")
    {
        // Ignore xsi:type attributes.
    }
    else
    {
        // [Log it...]
    }
}

// [And similarly for UnknownAttribute using e.Attr instead of e...]

另一个(hackier,IMO)解决方法是将xsi:type 映射到 BaseT 类中的虚拟属性:

[XmlInclude(typeof(DerivedT))]
public class BaseT
{
    [XmlAttribute("type", Namespace = XmlSchema.InstanceNamespace)]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)] // Hide this useless property
    public string XmlSchemaType
    {
        get { return null; } // Must return null for XmlSerializer.Serialize to work
        set { }
    }
}

【讨论】:

  • 只是好奇:您是否反汇编了生成的程序集?怎么样?
  • 是的,我做到了。 This answer 解释了如何让 XmlSerializer 持久化生成的程序集。我使用 JetBrains dotPeek 来反汇编结果。
【解决方案2】:

我认为这不是使用 XmlSerializer 的正确方法,即使您有带有警告的正确输出,但在更高级的场景中,不知道会出现什么问题。

您应该使用实际的派生类型 (aTypeObj.GetType()) 甚至泛型来进行排序。

【讨论】:

  • 您有支持该声明的文件吗?
  • 我不是真的,我只是说我就是这样做的,我所有的序列化例程都在帮助程序中定义,并在整个应用程序中重复使用,使用泛型或 .GetType()
  • 关键是,不可能知道对象具有什么特定的派生类型(请注意,在 user1892538 的回答之后,我验证了事件也被抛出给成员,而不仅仅是 XML 根)。然后Jon Skeet seems to say 我应该做我正在做的事情,所以我想我正在正确使用序列化程序。
  • 我认为您不应该提及我已删除的答案,如果您根本不提及我,我更愿意删除此评论。顺便说一句,我引用了 msdn:you can declare valid types only on a single field or property, instead of declaring derived types at the base class. You can attach XmlElement, XmlAttribute, or XmlArrayItem attributes to a field and declare the types that the field or property can reference. Then the constructor of the XmlSerializer will add the code required to serialize and deserialize those types to the serialization classes,并且我验证了未知事件不存在。
【解决方案3】:

您是否尝试过XMLSerializer 构造函数,您可以在其中将派生类型作为extraTypes 之一传递?

看这里:https://msdn.microsoft.com/en-us/library/e5aakyae%28v=vs.110%29.aspx

你可以这样使用它:

var serializer = new XmlSerializer(typeof(BaseT), new Type[] { typeof(DerivedT) });

当然,通常您可能希望从其他地方检索派生类型的列表。例如来自另一个程序集。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-07-11
    • 1970-01-01
    • 2012-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多