Why does my XML deserialization only recognise the first contained object tag

32 Views Asked by At

I have the following classes:

public interface IVariable
{
    string Name { get; }
    dynamic Value { get; set; }
}

public class Namespace : IVariable, IXmlSerializable, IEnumerable<IVariable>
{
    protected Namespace() { }

    public Namespace(string name, params string[] aliases)
    {
        this.Name = name;
        this._aliases.AddRange(aliases);
    }

    private readonly List<IVariable> _variables = new List<IVariable>();

    public string Name { get; private set; }

    dynamic IVariable.Value { get => this; set => throw new InvalidOperationException(); }

    private readonly List<string> _aliases = new List<string>();

    public void Add(IVariable variable)
    {
        _variables.Add(variable);
    }
    
    public dynamic this[string key]
    {
        get
        {
            foreach (var variable in _variables)
            {
                if (variable.Name == key) 
                    return variable.Value;

                if (variable is Namespace ns && ns._aliases.Contains(key))
                    return ns;          
            }

            return null;
        }
        set
        {
            foreach (var variable in _variables)
            {
                if (variable.Name == key) 
                    variable.Value = value;

                if (variable is Namespace ns && ns._aliases.Contains(key))
                    throw new InvalidOperationException();
            }
        }
    }

    void IXmlSerializable.WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("name", Name);

        foreach (var alias in _aliases)
        {
            writer.WriteElementString("Alias", alias);
        }

        foreach (var variable in _variables)
        {
            if (variable is Variable v)
            {
                writer.WriteStartElement("Variable");
                writer.WriteAttributeString("name", v.Name);
                writer.WriteAttributeString("typecode", v.TypeCode.ToString());
                writer.WriteString(v.Value.ToString());
                writer.WriteEndElement();
            }
            else
            {
                var serializer = new XmlSerializer(typeof(Namespace));
                serializer.Serialize(writer, (Namespace)variable);
            }
        }
    }

    XmlSchema IXmlSerializable.GetSchema()
    {
        return null;
    }

    void IXmlSerializable.ReadXml(XmlReader reader)
    {
        Name = reader.GetAttribute("name");

        var aliases = new List<string>();

        while (true)
        {
            reader.Read();

            if (reader.Name != "Alias") 
                break;

            reader.Read();
            _aliases.Add(reader.Value);
            reader.Read();
        }

        while (true)
        {
            if (reader.Name == "Variable")
            {
                var name = reader.GetAttribute("name");
                var typecode = (TypeCode)Enum.Parse(typeof(TypeCode), reader.GetAttribute("typecode"));
                reader.Read();
                var variable = new Variable(typecode, name);
                variable.SetValueFromString(reader.Value);

                Add(variable);
            }
            else if (reader.Name == "Namespace")
            {
                var serializer = new XmlSerializer(typeof(Namespace));
                _variables.Add((Namespace)serializer.Deserialize(reader));
            }
            else
            {
                break;
            }
        }
    }

    public IEnumerator<IVariable> GetEnumerator()
    {
        return ((IEnumerable<IVariable>)_variables).GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)_variables).GetEnumerator();
    }
}

A namespace can contain variables or other namespaces, a variable can hold one of a number of types as defined in Sysetem.TypeCode, and I need to be able to save the whole nested structure. The WriteXml method works as expected:

void Main()
{
    var stream = new MemoryStream();
    var serializer = new XmlSerializer(typeof(Namespace));

    var ns = new Namespace("foo", "f")
    {
        new Variable(TypeCode.String, "bar", "baz"),
        new Namespace("qux")
        {
            new Variable(TypeCode.Int32, "quxx", 42)
        },
    };

    serializer.Serialize(stream, ns);

    stream.Position = 0;
    Console.Out.WriteLine(new StreamReader(stream).ReadToEnd());
}

This code produces the following output:

<?xml version="1.0" encoding="utf-8"?>
<Namespace name="foo">
    <Alias>f</Alias>
    <Variable name="bar" typecode="String">baz</Variable>
    <Namespace name="qux">
        <Variable name="quxx" typecode="Int32">42</Variable>
    </Namespace>
</Namespace>

This would appear to be correct, but when I use a deserializer,

stream.Position = 0;
var ns2 = (Namespace)serializer.Deserialize(stream);

foreach (IVariable variable in ns2)
{
    Console.Out.WriteLine(variable.Name);
}

I only get the first member, bar. If I swap the order of the variable and namespace in the definition, I get the name of the namespace, but not the variable.

What am I doing wrong? I have tried adding a reader.Read() call both before and after the body of the while loop, and that just causes the deserializer to think the XML is broken.

Interestingly, I can add an arbitrary number of <Alias> tags without breaking anything.

1

There are 1 best solutions below

0
RoadieRich On BEST ANSWER

As suggested by @jdweng in a comment, I found a working solution by using XML Linq.

I rewrote the ReadXml method as follows:

void IXmlSerializable.ReadXml(XmlReader reader)
{
    if (!reader.IsStartElement()) return;
    Name = reader.GetAttribute("name");

    var element = (XElement)XElement.ReadFrom(reader);

    foreach (var e in element.Elements().Cast<XElement>())
    {
        if (e.Name == "Alias")
        {
            _aliases.Add(e.Value);
        }
        else if (e.Name == "Variable")
        {
            var name = e.Attribute(XName.Get("name")).Value;
            var type = (TypeCode)Enum.Parse(typeof(TypeCode), e.Attribute(XName.Get("typecode")).Value);
            var valueStr = e.Value;

            var variable = new Variable(type, name);
            variable.SetValueFromString(valueStr);

            Add(variable);
        }
        else if (e.Name == "Namespace")
        {
            var ns = (Namespace)new XmlSerializer(typeof(Namespace)).Deserialize(e.CreateReader());
            Add(ns);
        }
    }
}