XMLSerializer在反序列化派生类型时会警告未知的节点/属性。

3
我最近使用XMLSerializer注册了未知节点、元素和属性的事件处理程序,我用它来反序列化类型层次结构中的复杂类型。我这样做是因为我收到的某些XML来自第三方;我对可能导致我的问题的数据格式变化感兴趣。
在XML中,XMLSerializer使用标准的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 创建一个序列化程序,则警告消失。 - jsanalytics
1
@jstreet 但是这个属性的整个目的是使基类序列化程序能够反序列化派生对象。想象一下一个可以容纳任何派生类型的基类列表。调用代码不知道也不关心列表中实际包含哪些派生类型。实际上,处理列表的代码是在许多派生类型之前编写的。 - Peter - Reinstate Monica
2
我理解您的意思,序列化程序可以反序列化派生对象,但会出现“不便”,因为它实际上不知道anInt属性。建议:在创建序列化程序时,请使用aTypeObj.GetType()而不是使用任何显式类型,无论是基础类型还是派生类型。 - jsanalytics
1
@jstreet 警告并不是关于 anInt 的;该元素已经被正确地序列化和反序列化(当我在反序列化后测试非默认值时可以看到)。警告是关于属性 xsi:type 的。这些属性语法上可以携带对象信息(例如,我可以将 anInt 序列化为属性!),但序列化器使用它们来存储有关类型的元信息。属性 xmlns:xsixmlns:xsd 被正确地识别为“不属于对象数据的一部分”,但 xsi:type 却没有,我认为这是一个错误。 - Peter - Reinstate Monica
3个回答

6

我做错了什么吗?

我认为没有。我同意您的观点,即XmlSerializer应该在没有任何警告的情况下反序列化自己的输出。此外,xsi:typeXML Schema规范中定义的标准属性,显然它受到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...]

另一个(我认为比较hack的)解决方法是将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 { }
    }
}

只是好奇:你是否反汇编了生成的汇编代码?怎么做的? - Peter - Reinstate Monica
1
是的,我做到了。这个答案解释了如何使XmlSerializer持久化生成的程序集。我使用JetBrains dotPeek来反汇编结果。 - Michael Liu

0

我认为这不是使用XmlSerializer的正确方式,即使您在警告中获得了正确的输出,在更高级的场景中,无法预测会发生什么问题。

您应该使用实际派生类型(aTypeObj.GetType())或甚至泛型来解决此问题。


你有支持那个说法的文档吗? - Peter - Reinstate Monica
我并不是真的这么想,我只是在说我自己的做法,我的所有序列化例程都定义在帮助程序中,并且可以在应用程序中重复使用,采用泛型或.GetType()。 - Pedro Luz
2
重点是,不可能知道对象具体派生类型是什么(请注意,在user1892538的回答之后,我验证了事件也会为成员引发,而不仅仅是XML根目录)。然后,Jon Skeet似乎在说我应该正好做我正在做的事情,所以我想我正在正确使用序列化器。 - Peter - Reinstate Monica
我认为你不应该提到我已删除的答案,如果你完全不引用我,我更愿意删除这条评论。顺便说一下,我引用了MSDN:您只能在单个字段或属性上声明有效类型,而不是在基类上声明派生类型。您可以将XmlElement、XmlAttribute或XmlArrayItem属性附加到字段并声明字段或属性可以引用的类型。然后,XmlSerializer的构造函数将添加所需的代码以将这些类型序列化和反序列化到序列化类中,并且我验证了未知事件不存在。 - user6996876

0

你尝试过使用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) });

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接