2

I am trying to get the name of an XML tag into a class property when performing XML deserialization. I need the name as a property since multiple XML tags share the same class. The XML and associated classes are defined below.

I have an XML response which I receive in the format:

<Data totalExecutionTime="00:00:00.0467241">
    <ItemNumber id="1234" order="0" createdDate="2017-03-24T12:07:09.07" modifiedDate="2018-08-29T16:59:19.127">
        <Value modifiedDate="2017-03-24T12:07:12.77">ABC1234</Value>
        <Category id="5432" parentID="9876" itemOrder="0" modifiedDate="2017-03-24T12:16:23.687">The best category</Category>
        ... <!-- like 100 other elements -->
    </ItemNumber>
</Data>

Deserialize done as follows:

XmlSerializer serializer = new XmlSerializer(typeof(ItemData));
using (TextReader reader = new StringReader(response))
{
    ItemData itemData = (ItemData)serializer.Deserialize(reader);
}

And a class for the top level, ItemData:

[Serializable]
[XmlRoot("Data")]
public class ItemData
{
    [XmlAttribute("totalExecutionTime")]
    public string ExecutionTime { get; set; }

    [XmlElement("ItemNumber", Type = typeof(ItemBase))]
    public List<ItemBase> Items { get; set; }
}

ItemBase is defined as:

[Serializable]
public class ItemBase
{
    [XmlElement("Value")]
    public virtual ItemProperty ItemNumber { get; set; } = ItemProperty.Empty;

    [XmlElement("ItemName")]
    public virtual ItemProperty Category { get; set; } = ItemProperty.Empty;

    ... // like 100 other properties
}

And finally ItemProperty:

public class ItemProperty : IXmlSerializable
{
    public static ItemProperty Empty { get; } = new ItemProperty();

    public ItemProperty()
    {
        this.Name = string.Empty;
        this.Value = string.Empty;
        this.Id = 0;
        this.Order = 0;
    }

    public string Name { get; set; }

    [XmlText] // no effect while using IXmlSerializable
    public string Value { get; set; }

    [XmlAttribute("id")] // no effect while using IXmlSerializable
    public int Id { get; set; }

    [XmlAttribute("itemOrder")] // no effect while using IXmlSerializable
    public int Order { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        reader.MoveToContent();

        string name = reader.Name;
        this.Name = name;

        string val = reader.ReadElementString();
        this.Value = val;

        if (reader.HasAttributes)
        {
            string id = reader.GetAttribute("id");
            this.Id = Convert.ToInt32(id);

            string itemOrder = reader.GetAttribute("itemOrder");
            this.Order = Convert.ToInt32(itemOrder);

            string sequence = reader.GetAttribute("seq");
            this.Sequence = Convert.ToInt32(sequence);
        }

        // it seems the reader doesn't advance to the next element after reading
        if (reader.NodeType == XmlNodeType.EndElement && !reader.IsEmptyElement)
        {
            reader.Read();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        throw new NotImplementedException();
    }
}

The point of implementing the IXmlSerializable interface is because ultimately I need the name of the XML tag that is stored as an ItemProperty and that information is not captured when using the XML class/property attributes. I believe this is the case since the attributes determine which class to use for the deserialization and normally each XML tag would have an associated class. I don't want to go that direction since there are such a large number of different tags that may be in the response and they all share similar attributes. Hence the ItemProperty class.

I've tried also passing the name of the tag via a parameterized constructor in the ItemProperty and setting the name property there, but when deserialization is performed, it uses the default constructor and then sets the property values, so that is not an option.

Reflection doesn't work either since the class is always ItemProperty and therefore doesn't have a unique name.

Maybe how I'm structuring the XML attributes could be done differently to achieve what I'm trying to do, but I don't see it.

I'm open to any way to solve this problem, but I'm pretty sure it entails implementing IXmlSerializable.ReadXml(). I know it is the job of the XmlReader to read the entirety of the XML and advance the reader to the end of the text, but I'm a little unclear on how to do that.

TLDR: How do I properly implement IXmlSerializable.ReadXml() while capturing the XML tag name, tag value, and all attributes into the class properties?

Edit: with the updated ReadXml method, I get all the data needed at the ItemProperty level, but class ItemData, the Items list only ever has one item. I assume because I am not advancing the reader properly.

dbc
  • 104,963
  • 20
  • 228
  • 340
elusive
  • 460
  • 5
  • 24
  • Possibly related: [Xmlserializer to C# object, store original XML element](https://stackoverflow.com/q/50304153/3744182). Does that answer your question? It shows an example of loading the entire element corresponding to the `IXmlSerializable` node into an `XElement`. – dbc Dec 01 '18 at 00:48
  • Not exactly, since that example the element name being deserialized is known before runtime. I do get all of the data I need at the `ItemProperty` level, but I believe the reader isn't advancing since `ItemData.Items` only ever contains one item. See the edit at the bottom. Thanks. – elusive Dec 04 '18 at 21:18

1 Answers1

3

From the documentation for IXmlSerializable.ReadXml(XmlReader):

When this method is called, the reader is positioned on the start tag that wraps the information for your type. ... When this method returns, it must have read the entire element from beginning to end, including all of its contents. Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.

Your ReadXml() can be modified to meet these requirements as follows:

public void ReadXml(XmlReader reader)
{
    reader.MoveToContent();

    this.Name = reader.LocalName; // Do not include the prefix (if present) in the Name.

    if (reader.HasAttributes)
    {
        var id = reader.GetAttribute("id");
        if (id != null)
            // Since id is missing from some elements you might want to make it nullable
            this.Id = XmlConvert.ToInt32(id);

        var order = reader.GetAttribute("itemOrder");
        if (order != null)
            // Since itemOrder is missing from some elements you might want to make it nullable
            this.Order = XmlConvert.ToInt32(order);

        string sequence = reader.GetAttribute("seq");
        //There is no Sequence property?
        //this.Sequence = Convert.ToInt32(sequence);
    }

    // Read element value.
    // This method reads the start tag, the contents of the element, and moves the reader past the end element tag.
    // thus there is no need for an additional Read()
    this.Value = reader.ReadElementContentAsString();
}

Notes:

  1. You are calling ReadElementString() whose documentation states:

    We recommend that you use the ReadElementContentAsString() method to read a text element.

    As suggested, I modified your ReadXml() to use this method. In turn, its documentation states:

    This method reads the start tag, the contents of the element, and moves the reader past the end element tag.

    Thus this method should leave the XmlReader positioned exactly as required by ReadXml(), ensuring the reader is advanced properly.

  2. The XML attributes of each ItemProperty element must be processed before that element's content is read, since reading the content advances the reader past the element start -- and its attributes.

  3. Utilities from the XmlConvert class should be used to parse and format XML primitives so that numerical and date/time values are not erroneously localized.

  4. You probably don't want to include the namespace prefix (if any) in the Name property.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340