如何使用 XmlSerializer 序列化派生实例?

How to use XmlSerializer to serialize derived instances?

我意识到这看起来与 Using XmlSerializer to serialize derived classes 完全相同,但我不知道如何按照同一问题的指导进行操作:

using System;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace xmlSerializerLab
{
    public class Utf8StringWriter : System.IO.StringWriter
    {
        public override Encoding Encoding => Encoding.UTF8;
    }

    [XmlRoot(ElementName = "Query", Namespace = "http://www.opengis.net/wfs")]
    public class Query
    {
        [XmlElement(ElementName = "Filter", Namespace = "http://www.opengis.net/ogc")]
        public Filter Filter { get; set; }
    }

    [XmlInclude(typeof(PropertyIsOpFilter))]
    [XmlInclude(typeof(PropertyIsEqualToFilter))]
    [XmlInclude(typeof(OpFilterBase))]
    [XmlInclude(typeof(LiteralFilter))]
    [XmlInclude(typeof(Query))]
    [Serializable]
    public class Filter
    {
        [XmlElement]
        public Filter And { get; set; }
    }

    public class PropertyIsOpFilter : Filter, IXmlSerializable
    {

        public Filter LeftOp { get; set; }

        public Filter RightOp { get; set; }

        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader) { }

        public void WriteXml(XmlWriter writer)
        {
            Program.ToXml(LeftOp, writer);
            Program.ToXml(RightOp, writer);
        }
    }

    [XmlRoot("IsEqualTo")]
    public class PropertyIsEqualToFilter : PropertyIsOpFilter { }

    public class OpFilterBase : Filter, IXmlSerializable
    {
        public string Op { get; set; }
        public object Value { get; set; }

        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader) { }

        public void WriteXml(XmlWriter writer)
        {
            if (!String.IsNullOrEmpty(Op))
            {
                writer.WriteStartElement(Op);
                writer.WriteValue(Value);
                writer.WriteEndElement();
            }
            else
            {
                writer.WriteValue(Value);
            }
        }
    }

    public class LiteralFilter : OpFilterBase { }


    class Program
    {
        public static void ToXml(Object o, XmlWriter writer)
        {
            var inputSerializer = new XmlSerializer(o.GetType(), new Type[] {
                typeof(Filter),
                typeof(PropertyIsOpFilter),
                typeof(PropertyIsEqualToFilter),
                typeof(OpFilterBase),
                typeof(LiteralFilter),
                typeof(Query)
            });
            inputSerializer.Serialize(writer, o);
        }

        public static string ToXml(Object o)
        {
            var inputSerializer = new XmlSerializer(o.GetType());
            using (var writer = new Utf8StringWriter())
            {
                using (var xmlWriter = new XmlTextWriter(writer))
                {
                    ToXml(o, xmlWriter);
                }
                return writer.ToString();
            }
        }

        static void Main(string[] args)
        {
            Filter o = new PropertyIsEqualToFilter()
            {
                LeftOp = new LiteralFilter()
                {
                    Value = 1
                },
                RightOp = new LiteralFilter()
                {
                    Value = 1
                }
            };

            var query = new Query()
            {
                Filter = o
            };

            Console.WriteLine(ToXml(query));
            Console.ReadLine();
        }
    }
}

导致此异常:

InvalidOperationException: The type xmlSerializerLab.PropertyIsEqualToFilter may not be used in this context. To use xmlSerializerLab.PropertyIsEqualToFilter as a parameter, return type, or member of a class or struct, the parameter, return type, or member must be declared as type xmlSerializerLab.PropertyIsEqualToFilter (it cannot be object). Objects of type xmlSerializerLab.PropertyIsEqualToFilter may not be used in un-typed collections, such as ArrayLists.

据我所知,我需要 PropertyIsOpFilter 和 OpFilterBase 上的 IXmlSerializable,因为我正在尝试以 this schema 描述的特定 XML 格式为目标。但我发现我还必须使查询 class IXmlSerializable.

这是我希望能够从模型中生成的示例 XML 文档:

<GetFeature xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  service="WFS"
  version="1.1.0"
  maxFeatures="0" xmlns="http://www.opengis.net/wfs">
  <ResultType>Results</ResultType>
  <OutputFormat>text/gml; subtype=gml/3.1.1</OutputFormat>
  <Query
    d2p1:srsName="EPSG:4326" xmlns:d2p1="http://www.opengis.net/ogc">
    <d2p1:Filter>
      <d2p1:IsEqualTo>
        <d2p1:PropertyName>Prop1</d2p1:PropertyName>
        <d2p1:Literal>1</d2p1:Literal>
      </d2p1:IsEqualTo>
    </d2p1:Filter>
  </Query>
</GetFeature>

通过使查询 class IXmlSerializable 并编写大量的 WriteXml 和 ReadXml 逻辑,我可以让它工作,但我希望它可以工作而不必执行所有这些操作,因为 XmlRoot 和XmlAttribute 和 XmlElement 标签应该为序列化程序提供足够的信息,以便它根据标签名称(匹配 ElementName)知道要实例化哪个 class 以及如何根据属性进行序列化。

您看到的问题 可以通过以下最小示例重现:

public class BaseClass
{
}

public class DerivedClass : BaseClass, IXmlSerializable
{
    #region IXmlSerializable Members

    public XmlSchema GetSchema() { return null; }

    public void ReadXml(XmlReader reader) { throw new NotImplementedException(); }

    public void WriteXml(XmlWriter writer) { }

    #endregion
}

使用序列化代码:

BaseClass baseClass = new DerivedClass();

using (var textWriter = new StringWriter())
{
    using (var xmlWriter = XmlWriter.Create(textWriter))
    {
        var serializer = new XmlSerializer(typeof(BaseClass), new Type[] { typeof(DerivedClass) });
        serializer.Serialize(xmlWriter, baseClass);
    }
    Console.WriteLine(textWriter.ToString());
}

抛出以下异常(样本fiddle #1):

System.InvalidOperationException: There was an error generating the XML document. 
---> System.InvalidOperationException: The type DerivedClass may not be used in this context. To use DerivedClass as a parameter, return type, or member of a class or struct, the parameter, return type, or member must be declared as type DerivedClass (it cannot be object). Objects of type DerivedClass may not be used in un-typed collections, such as ArrayLists.

这是我从 XmlSerializer 看到的最无用的异常消息之一。要了解异常,您需要了解 XmlSerializer 如何通过 [XmlInclude] mechanism. If I remove IXmlSerializable from DerivedClass, the following XML is generated (fiddle #2):

处理多态性
<BaseClass xsi:type="DerivedClass" />

注意到 xsi:type 类型属性了吗?那是您正在使用的 w3c standard attribute that XmlSerializer uses to explicitly assert the type of a polymorphic element; it is documented here. When XmlSerializer is deserializing a polymorphic type to which [XmlInclude] attributes have been applied (either statically or through the constructor),它将查找 xsi:type 属性以确定要构造和反序列化的实际类型。

显然,这就是与IXmlSerializable冲突的地方。实现该接口的类型应该完全控制它的XML读写。但是,通过xsi:type属性的解析和解释,XmlSerializer已经开始自动反序列化,因此由于base和base的反序列化策略不一致而抛出异常派生类型。

更重要的是,将IXmlSerializable添加到基本类型也不能真正解决问题如果你这样做,xsi:type属性永远不会被写入,后来,当ReadXml()被调用时,将无条件地构造一个基类型的对象,如fiddle #3.

所示

(可以想象,微软可以实施一种特殊情况,其中 XmlSerializer 开始自动反序列化,然后 "backs off" 并在 IXmlSerializable 时将任务交给 ReadXml()遇到并构造了多态类型。但是,他们没有。)

解决方案 似乎是使用[XmlInclude] 机制自动序列化您的Filter 类型。事实上,我没有看到任何你需要使用 IXmlSerializable 的理由,并且能够通过完全删除 IXmlSerializable 并对命名空间进行一些小的更改来成功序列化你的模型:

public static class XmlNamespaces
{
    public const string OpengisWfs = "http://www.opengis.net/wfs";
}

[XmlRoot(Namespace = XmlNamespaces.OpengisWfs)]
public class Query
{
    public Filter Filter { get; set; }
}

[XmlInclude(typeof(PropertyIsOpFilter))]
[XmlInclude(typeof(OpFilterBase))]
[XmlRoot(Namespace = XmlNamespaces.OpengisWfs)]
public class Filter
{
    [XmlElement]
    public Filter And { get; set; }
}

[XmlInclude(typeof(PropertyIsEqualToFilter))]
[XmlRoot(Namespace = XmlNamespaces.OpengisWfs)]
public class PropertyIsOpFilter : Filter
{
    public Filter LeftOp { get; set; }

    public Filter RightOp { get; set; }
}

[XmlRoot("IsEqualTo", Namespace = XmlNamespaces.OpengisWfs)]
public class PropertyIsEqualToFilter : PropertyIsOpFilter { }

[XmlInclude(typeof(LiteralFilter))]
[XmlRoot(Namespace = XmlNamespaces.OpengisWfs)]
public class OpFilterBase : Filter
{
    public string Op { get; set; }
    public object Value { get; set; }
}

[XmlRoot(Namespace = XmlNamespaces.OpengisWfs)]
public class LiteralFilter : OpFilterBase { }

备注:

  • 要使 [XmlInclude] 机制起作用,所有包含的类型显然必须与基类型位于相同的 XML 命名空间中。为了确保这一点,我将 [XmlRoot(Namespace = XmlNamespaces.OpengisWfs)] 添加到所有 Filter 子类型。

  • [XmlInclude(typeof(DerivedType))] 属性可以添加到它们的直接父类型或最低公共基类型。在上面的代码中,我将属性添加到直接父类型,以便可以成功序列化中间类型的成员,例如:

    public class SomeClass
    {
        PropertyIsOpFilter  IsOpFilter { get; set; }
    }
    
  • 考虑将无法实例化的中间类型标记为 abstract,例如public abstract class Filter。考虑将 "most derived" 类型标记为 sealed,例如public sealed class LiteralFilter

  • 如果使用 new XmlSerializer(Type, Type []) 构造函数,则必须静态缓存序列化程序以避免严重的内存泄漏,如 here 所述。在我的解决方案中没有必要,但您在问题中使用了它。

示例 fiddle #4 显示以下 XML 已成功生成:

<Query xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.opengis.net/wfs">
  <Filter xsi:type="PropertyIsEqualToFilter">
    <LeftOp xsi:type="LiteralFilter">
      <Value xsi:type="xsd:int">1</Value>
    </LeftOp>
    <RightOp xsi:type="LiteralFilter">
      <Value xsi:type="xsd:int">1</Value>
    </RightOp>
  </Filter>
</Query>