1

I'm facing an issue with the .Net XmlSerializer, basically I need one or more elements with same name to be serialized (and deserialized) dynamically between other elements of fixed schema.

Example:

<?xml version="1.0" encoding="utf-8"?>
<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <asd>asd</asd>
  <nnn>q</nnn>
  <nnn>w</nnn>
  <nnn>e</nnn>
  <aaa>aaa</aaa>
</A>

My real <nnn> tag is a little bit more complicated, with dynamic tags inside (not only conditionals), but I'm currently threating this right. I really need to use the "Order" parameter of XmlElement to control some rules.

I can't change the XML layout.

The example serializable class:

[XmlRoot]
[Serializable]
public class A
{
    [XmlElement("asd", Order=1)]
    public string asd { get; set; }

    [XmlIgnore]
    public string[] qwe { get; set; }

    [XmlAnyElement("nnn", Order=2)]
    public XmlNode[] nnn
    {
        get
        {
            if (qwe == null) return null;

            var xml = new XmlDocument();
            var nodes = new List<XmlNode>(qwe.Length);

            foreach (var q in qwe)
            {
                var nnnTag = xml.CreateNode(XmlNodeType.Element, "nnn", null);
                nnnTag.InnerText = q;
                nodes.Add(nnnTag);
            }

            return nodes.ToArray();
        }
        set
        {
            if (value == null) return;
            qwe = value.Select(tag => tag.InnerText).ToArray();
        }
    }

    [XmlElement("aaa", Order=3)]
    public string aaa { get; set; }

The problem is, when not using the "Order" parameter the serialization goes fine, but with the parameter the elements after the XmlAnyElement are understood as part of the array of nodes and so are not deserialized right.

My main program for the example is a Console Application with the following main:

static void Main(string[] args)
{
    var a = new A
    {
        aaa = "aaa",
        asd = "asd",
        qwe = new[] {"q", "w", "e"}
    };
    var s = Serialize(a);
    var ss = Deserialize<A>(s);
    var s2 = Serialize(ss);

    Console.WriteLine(s);
    Console.WriteLine(s2);

    Console.WriteLine("Equals: {0};", s == s2);

    Console.ReadKey();
}

The wrong output is:

<?xml version="1.0" encoding="utf-8"?>
<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <asd>asd</asd>
  <nnn>q</nnn>
  <nnn>w</nnn>
  <nnn>e</nnn>
  <aaa>aaa</aaa>
</A>
<?xml version="1.0" encoding="utf-8"?>
<A xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <asd>asd</asd>
  <nnn>q</nnn>
  <nnn>w</nnn>
  <nnn>e</nnn>
  <nnn>aaa</nnn>
</A>
Equals: False;

For testing, here is the Serialization/Deserialization snippets I'm using:

public static string Serialize<T>(T a)
{
    var s = new XmlSerializer(typeof(T));
    using (var ms = new MemoryStream())
    {
        using (TextWriter sw = new StreamWriter(ms))
        {
            s.Serialize(sw, a);
            ms.Seek(0, 0);
            using (var sr = new StreamReader(ms))
            {
                return sr.ReadToEnd();
            }
        }
    }
}

public static T Deserialize<T>(string a)
{
    var s = new XmlSerializer(typeof(T));
    var bytes = Encoding.ASCII.GetBytes(a);
    using (var ms = new MemoryStream(bytes))
    {
        return (T) s.Deserialize(ms);
    }
}

The full source code: https://gist.github.com/inventti-gabriel/81054269f2e0a32d7e8d1dd44f30a97f

Thanks in advance.

dbc
  • 104,963
  • 20
  • 228
  • 340
gariel
  • 687
  • 3
  • 13
  • 2
    This does look like a bug to me. Can you give more specifics as to what `nnn` really is? Because you could [change to this](https://dotnetfiddle.net/E2b4xp) for the example, but that may not work in your 'real' case. As an aside, note in the example you can simplify your serialize/deserialize methods to use `StringWriter`/`StringReader`. – Charles Mager Jun 20 '17 at 13:27

2 Answers2

1

I agree with @CharlesMager that this does appear to be a bug in XmlSerializer.

That being said, you can work around the bug by introducing an intermediate wrapper class or struct to contain the arbitrary nodes, then modifying your surrogate nnn property to return an array of these structs after marking the property with [XmlElement("nnn", Order = 2)]:

[XmlRoot]
[Serializable]
public class A
{
    [XmlElement("asd", Order = 1)]
    public string asd { get; set; }

    [XmlIgnore]
    public string[] qwe { get; set; }

    [XmlElement("nnn", Order = 2)]
    public XmlNodeWrapper [] nnn
    {
        get
        {
            if (qwe == null)
                return null;

            var xml = new XmlDocument();
            var nodes = new List<XmlNode>(qwe.Length);

            foreach (var q in qwe)
            {
                var nnnTag = xml.CreateNode(XmlNodeType.Element, "nnn", null);
                nnnTag.InnerText = q;
                nodes.Add(nnnTag);
            }

            return nodes.Select(n => (XmlNodeWrapper)n.ChildNodes).ToArray();
        }
        set
        {
            if (value == null)
                return;
            qwe = value.Select(tag => tag.InnerText()).ToArray();
        }
    }

    [XmlElement("aaa", Order = 3)]
    public string aaa { get; set; }
}

[XmlType(AnonymousType = true)]
public struct XmlNodeWrapper
{
    public static implicit operator XmlNodeWrapper(XmlNodeList nodes)
    {
        return new XmlNodeWrapper { Nodes = nodes == null ? null : nodes.Cast<XmlNode>().ToArray() };
    }

    public static implicit operator XmlNode[](XmlNodeWrapper wrapper)
    {
        return wrapper.Nodes;
    }

    // Marking the Nodes property with both [XmlAnyElement] and [XmlText] indicates that the node array
    // may contain mixed content (I.e. both XmlElement and XmlText objects).
    // Hat tip: https://stackoverflow.com/questions/25995609/xmlserializer-node-containing-text-xml-text 
    [XmlAnyElement]
    [XmlText]
    public XmlNode[] Nodes { get; set; }

    public string InnerText()
    {
        if (Nodes == null)
            return null;
        return String.Concat(Nodes.Select(n => n.InnerText));
    }
}

Note that the Nodes property in XmlNodeWrapper is marked with both [XmlAnyElement] and [XmlText]. The requirement to do this is explained here.

Sample fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
0

Try using XmlArrayAttribute in place of XmlAnyElement as the nodes are array

Test12345
  • 1,625
  • 1
  • 12
  • 21