How to use XmlSerializer to serialize derived instances?

巧了我就是萌 提交于 2020-08-08 07:25:43

问题


I realize this looks to be an exact duplicate of Using XmlSerializer to serialize derived classes, but I cannot figure out how to get this working following the guidance from that same question:

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();
        }
    }
}

It results in this exception:

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.

As far as I can tell, I need the IXmlSerializable on the PropertyIsOpFilter and OpFilterBase because I'm trying to target a specific XML format described by this schema. But I'm finding that I also have to make the Query class IXmlSerializable.

Here is a sample XML document that I'd like to be able to produce from the model:

<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>

By making the Query class IXmlSerializable and writing a good bit of WriteXml and ReadXml logic I can get it to work, but I'd expect it to work without having to do all that since the XmlRoot and XmlAttribute and XmlElement tags should give enough information to the serializer for it to know which class to instantiate based on the tag name (match ElementName) and certainly how to serialize based on the attributes.


回答1:


The problem you are seeing can be reproduced with the following minimal example:

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
}

Using the serialization code:

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());
}

The following exception is thrown (sample 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.

This is among the most unhelpful exception messages I have seen from XmlSerializer. To understand the exception, you need to understand how XmlSerializer handles polymorphism via the [XmlInclude] mechanism. If I remove IXmlSerializable from DerivedClass, the following XML is generated (fiddle #2):

<BaseClass xsi:type="DerivedClass" />

Notice the xsi:type type attribute? That is a 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 you are using), it will look for the xsi:type attribute to determine the actual type to construct and deserialize.

It is this, apparently, which conflicts with IXmlSerializable. A type which implements this interface should completely control its XML reading and writing. However, by parsing and interpreting the xsi:type attribute, XmlSerializer has already begun automatic deserialization, and so throws an exception due to the inconsistent deserialization strategies of the base and derived types.

What's more, adding IXmlSerializable to the base type doesn't really fix the problem either If you do so, the xsi:type attribute is never written, and later, when ReadXml() is called, an object of the base type will get unconditionally constructed, as shown in fiddle #3.

(It's conceivable that Microsoft could have implemented a special case where XmlSerializer begins automatic deserialization, then "backs off" and hands the task over to ReadXml() when an IXmlSerializable polymorphic type is encountered and constructed. But, they did not.)

The solution would seem to be to serialize your Filter types automatically using the [XmlInclude] mechanism. In fact I don't see any reason you need to use IXmlSerializable, and was able to serialize your model successfully by removing IXmlSerializable completely and making some minor changes to namespaces:

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 { }

Notes:

  • For the [XmlInclude] mechanism to work, all the included types apparently must be in the same XML namespace as the base type. To ensure this I added [XmlRoot(Namespace = XmlNamespaces.OpengisWfs)] to all the Filter subtypes.

  • The [XmlInclude(typeof(DerivedType))] attributes can be added either to their immediate parent type or to the lowest common base type. In the code above I added the attributes to the immediate parent types so that members of an intermediate type could be serialized successfully, e.g.:

    public class SomeClass
    {
        PropertyIsOpFilter  IsOpFilter { get; set; }
    }
    
  • Consider marking intermediate types that cannot be instantiated as abstract, e.g. public abstract class Filter. Consider marking types that are "most derived" as sealed, e.g. public sealed class LiteralFilter

  • If you use the new XmlSerializer(Type, Type []) constructor, you must statically cache the serializer to avoid a severe memory leak, as explained here. It's not necessary in my solution but you are using it in your question.

Sample fiddle #4 showing that the following XML is generated successfully:

<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>


来源:https://stackoverflow.com/questions/48427634/how-to-use-xmlserializer-to-serialize-derived-instances

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!