0

I'm trying to test the serialization for webrequests. I'm doing a unit test where I: - create a mock response from the server - deserialize that response - Compare initial object with deserialized one

The issue is one of my arrays in only partially populated where instead of all the elements, it only has one, the last one. Deserialization has to be made by hand because of xml schema limitations. Item is a partial class to separate the DTO from the xml operations

I have tried to change the attributes of the array property to

[XmlArray("items")]
[XmlArrayItemAttribute("item")]

I have tested just the serialization-deserialization of an individual item and it works.

I have checked both the xml resulting from the original serialization and the xml which is deseriliazed and they are equal.

First the item itself:

[XmlRoot("item")]
public partial class InvoiceItem
{      
    [XmlElement(ElementName = "name")]
    public string Name { get; set; }    
}

Now the array:

    [XmlArray("items")]
    [XmlArrayItemAttribute("item")]
    public InvoiceItem[] InvoiceItems
    {
        get
        {
            return this.invoiceItems;
        }
        set
        {
            this.invoiceItems = value;
        }

    }

Finally the deserializator:

    public void ReadXml(XmlReader reader)
    {
        Regex regexTaxName = new Regex(@"tax\d_name");
        Regex regexTaxType = new Regex(@"tax\d_type");
        Regex regexTaxPercent = new Regex(@"tax\d_percent");
        Regex regexTaxNumber = new Regex(@"\d");
        List<Tax> taxesList = new List<Tax>();
        while (reader.Read())
        {
            Debug.WriteLine(reader.GetAttribute("name"));
            if (reader.NodeType == XmlNodeType.Element)
            {

                if (reader.Name.Equals("name", StringComparison.Ordinal))
                {
                    reader.Read();
                    this.Name = reader.Value;
                }
                else if (reader.Name.Equals("type", StringComparison.Ordinal))
                {
                    ProductType value = ProductType.Product;
                    reader.Read();
                    if (Enums.TryParse<ProductType>(reader.Value, out value))
                    {

                        this.Type = value;
                    }
                }
                else if (reader.Name.Equals("description", StringComparison.Ordinal))
                {
                    reader.Read();
                    this.Description = reader.Value;
                }
                else if (reader.Name.Equals("unit_cost", StringComparison.Ordinal))
                {
                    float value = 0;
                    reader.Read();
                    if (float.TryParse(reader.Value, out value))
                    {

                        this.UnitCost = value;
                    }
                }
                else if (reader.Name.Equals("quantity", StringComparison.Ordinal))
                {
                    int value = 0;
                    reader.Read();
                    if (int.TryParse(reader.Value, out value))
                    {

                        this.Quantity = value;
                    }
                }
                else if (reader.Name.Equals("discount", StringComparison.Ordinal))
                {
                    float value = 0;
                    reader.Read();
                    if (float.TryParse(reader.Value, out value))
                    {

                        this.Discount = value;
                    }
                }
                else if (reader.Name.Equals("discount_type", StringComparison.Ordinal))
                {
                    NumericalSignificance value = NumericalSignificance.Percent;
                    reader.Read();
                    if (Enums.TryParse<NumericalSignificance>(reader.Value, out value))
                    {

                        this.DiscountType = value;
                    }
                }
                else if (regexTaxName.IsMatch(reader.Name))
                {
                    int taxNumber = int.Parse(regexTaxNumber.Match(reader.Name).Value, CultureInfo.CurrentCulture);

                    if (taxesList.Count < taxNumber)
                    {
                        reader.Read();
                        Tax newTax = new Tax();
                        newTax.Name = reader.Value;
                        taxesList.Add(newTax);
                    }
                    else
                    {
                        reader.Read();
                        taxesList[taxNumber-1].Name = reader.Value;
                    }
                }
                else if (regexTaxPercent.IsMatch(reader.Name))
                {
                    int taxNumber = int.Parse(regexTaxNumber.Match(reader.Name).Value, CultureInfo.CurrentCulture);
                    if (taxesList.Count > taxNumber)
                    {
                        Tax newTax = new Tax();
                        float value = 0;
                        reader.Read();
                        if (float.TryParse(reader.Value, out value))
                        {

                            newTax.TaxPercent = value;
                        }

                        taxesList.Add(newTax);
                    }
                    else
                    {
                        float value = 0;
                        reader.Read();
                        if (float.TryParse(reader.Value, out value))
                        {

                            taxesList[taxNumber-1].TaxPercent = value;
                        }
                    }
                }
                else if (regexTaxType.IsMatch(reader.Name))
                {
                    int taxNumber = int.Parse(regexTaxNumber.Match(reader.Name).Value, CultureInfo.CurrentCulture);
                    if (taxesList.Count > taxNumber)
                    {
                        Tax newTax = new Tax();
                        NumericalSignificance value = NumericalSignificance.Percent;
                        reader.Read();
                        if (Enums.TryParse<NumericalSignificance>(reader.Value, out value))
                        {

                            newTax.Type = value;
                        }

                        taxesList.Add(newTax);
                    }
                    else
                    {
                        NumericalSignificance value = NumericalSignificance.Percent;
                        reader.Read();
                        if (Enums.TryParse<NumericalSignificance>(reader.Value, out value))
                        {

                            taxesList[taxNumber-1].Type = value;
                        }
                    }
                }
            }
        }
        taxesArr = taxesList.ToArray();
    }

The problems is on the items array where the end object only has the final object instead of all the original ones.

EDIT:

An example that shows the issue:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;


[XmlRoot("invoice")]
public class Invoice
{
    [XmlArray("items")]
    private InvoiceExampleItem[] items;

    [XmlArray("items")]
    public InvoiceExampleItem[] Items
    {
        get { return this.items; }
        set { this.items = value; }
    }
}

[XmlRoot("item", Namespace = "")]
public partial class InvoiceExampleItem : IXmlSerializable
{
    [XmlElement(ElementName = "name")]
    public string Name { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        while (reader.Read())
        {
            if (reader.NodeType == XmlNodeType.Element)
            {

                if (reader.Name.Equals("name", StringComparison.Ordinal))
                {
                    reader.Read();
                    this.Name = reader.Value;
                }
            }
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteElementString("name", this.Name);
    }
}




    [TestClass]
    public class ExampleTest : AutoMoqTest
    {
        [TestMethod]
        public void ExampleDeserialization()
        {
            InvoiceExampleItem item1 = new InvoiceExampleItem()
            {
                Name = "item1"
            };

            InvoiceExampleItem item2 = new InvoiceExampleItem()
            {
                Name = "item2"
            };

            InvoiceExampleItem item3 = new InvoiceExampleItem()
            {
                Name = "item3"
            };


            Invoice mockInvoice = new Invoice()
            {
                Items = new InvoiceExampleItem[] { item1, item2, item3 }
            };
            XmlDocument mockInvoiceSerialized = SerializeInvoice(mockInvoice);

            XmlDocument mockResponseXml = GenerateXmlResponse(mockInvoiceSerialized);

            GetInvoiceResponse response = DeserializeXML<GetInvoiceResponse>(mockResponseXml.OuterXml);

            Invoice resultInvoice = response.Invoice;

            if (mockInvoice.Items.Length != resultInvoice.Items.Length)
            {
                throw new Exception("wrong number of items");
            }

        }

        public XmlDocument SerializeInvoice(Invoice invoiceToSerialize)
        {
            XmlDocument toReturn = new XmlDocument();
            XmlSerializer serializer = new XmlSerializer(typeof(Invoice));
            XmlReaderSettings settings = new XmlReaderSettings();

            XmlDocument itemsDocument = GetTemplateXML();
            InvoiceExampleItem[] items = invoiceToSerialize.Items;
            MemoryStream memStm = null, tempStream = null;
            try
            {
                memStm = tempStream = new MemoryStream();

                using (StreamWriter sw = new StreamWriter(memStm, Encoding.UTF8))
                {
                    // Serialize object into raw xml
                    memStm = null;
                    serializer.Serialize(sw, invoiceToSerialize);
                    sw.Flush();

                    // parse raw xml into Xml document
                    tempStream.Position = 0;
                    settings.IgnoreWhitespace = true;
                    var xtr = XmlReader.Create(tempStream, settings);

                    toReturn.Load(xtr);
                }
            }
            finally
            {
                if (memStm != null)
                {
                    memStm.Dispose();
                }
            }

            return toReturn;
        }

        public static T DeserializeXML<T>(string responseString)
            where T : class
        {
            XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
            using (StringReader sr = new StringReader(responseString))
            {
                return (T)xmlSerializer.Deserialize(sr);
            }
        }

        public XmlDocument GetTemplateXML()
        {
            XmlDocument toReturn = new XmlDocument();
            var decl = toReturn.CreateXmlDeclaration("1.0", "utf-8", string.Empty);
            toReturn.AppendChild(decl);

            return toReturn;
        }

        private XmlDocument GenerateXmlResponse(XmlDocument innerXMLDocument)
        {
            XmlDocument toReturn = GetTemplateXML();

            XmlElement requestElement = toReturn.CreateElement("response");
            requestElement.SetAttribute("status", "success");
            requestElement.InnerXml = innerXMLDocument.DocumentElement.OuterXml;
            toReturn.AppendChild(requestElement);

            return toReturn;
        }

        /// <summary>
        /// Web response from creating a invoice
        /// </summary>
        [System.Xml.Serialization.XmlTypeAttribute(Namespace = "")]
        [System.Xml.Serialization.XmlRootAttribute(Namespace = "", ElementName = "response")]
        public class GetInvoiceResponse
        {
            /// <summary>
            /// Gets or sets the response status
            /// </summary>
            /// <value>
            /// reponse Status
            /// </value>
            [XmlAttribute("status")]
            public string ResponseStatus { get; set; }

            /// <summary>
            /// Gets or sets the new invoice id
            /// </summary>
            /// <value>
            /// generated by invoicera for this response
            /// </value>
            [XmlElement(ElementName = "invoice")]
            public Invoice Invoice { get; set; }
        }
    }
User23145
  • 11
  • 4
  • 1
    At a glance, deserialising a single item in a while loop with the condition `while (reader.Read())` is just going to keep reading until the end of the file overwriting all your properties each time until you're left with the last one. I'd strongly suggest just using the built-in serialisation attributes & either adding additional properties that do any extra logic or projecting this DTO into another model entirely after deserialising. Fiddling with `XmlReader` at this low level is very fiddly and error prone and will be hard to maintain. – Charles Mager Feb 08 '19 at 12:24
  • While I agree with you that I should just use the default deserializer, the manner in which the full item xml is written (with incorrect structure) which I do not control forces me to do this with a custom serializer. Basically an invoice has an array of items. I m using the default deserializer for the invoice and a custom one for the individual item . – User23145 Feb 08 '19 at 12:47
  • How far down are you nested at this point? Abandoning `XmlSerializer` altogether and using LINQ to XML might be easier. – Charles Mager Feb 08 '19 at 13:36
  • I am at the first level really. Invoice contains an array of items. But the item is correctly formed. – User23145 Feb 08 '19 at 18:44
  • Can you share a [mcve]? – dbc Feb 09 '19 at 03:17
  • @dbc I edited the question with a minimal example – User23145 Feb 12 '19 at 12:05
  • If IXMLSerializable is removed from the item class, the array is correctly populated, but it is mandatory for it to be implemented due to contraints of the xml (not under my control) – User23145 Feb 12 '19 at 16:43

1 Answers1

1

The solution was to create a class for the array and implement the IXMLSeriazable interface in that class and removing the interface from the item class.

Then, when the xml in the items class, I cycle the tags and create an item individually, adding it to the array next.

For some reason, the method was not exiting when it processed each <\item> tag, so I added a condition to exit the cycle.

Here is the complete code:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

[XmlRoot("invoice")]
public class Invoice
{
    public Items items { get; set; }

}

[XmlRoot("item", Namespace = "")]
public partial class InvoiceExampleItem
{
    [XmlElement(ElementName = "name")]
    public string Name { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        while (reader.Read())
        {
            if (reader.NodeType == XmlNodeType.Element)
            {

                if (reader.Name.Equals("name", StringComparison.Ordinal))
                {
                    reader.Read();
                    this.Name = reader.Value;
                    return;
                }
            }
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteElementString("name", this.Name);
    }
}


public class Items : IXmlSerializable
{
    [XmlElement("item")]
    public List<InvoiceExampleItem> list = new List<InvoiceExampleItem>();

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        while (reader.ReadToFollowing("item"))
        {
            InvoiceExampleItem currentItem = new InvoiceExampleItem();
            currentItem.Name = reader.Value;
            list.Add(currentItem);
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        foreach(InvoiceExampleItem item in list)
        {
            writer.WriteStartElement("item");
            item.WriteXml(writer);
            writer.WriteEndElement();

        }
    }
}


    [TestClass]
    public class ExampleTest : AutoMoqTest
    {
        [TestMethod]
        public void ExampleDeserialization()
        {
            /**/
            InvoiceExampleItem item1 = new InvoiceExampleItem()
            {
                Name = "item1"
            };

            InvoiceExampleItem item2 = new InvoiceExampleItem()
            {
                Name = "item2"
            };

            InvoiceExampleItem item3 = new InvoiceExampleItem()
            {
                Name = "item3"
            };

        Items items = new Items();
        InvoiceExampleItem item21 = new InvoiceExampleItem()
        {
            Name = "item1"
        };

        InvoiceExampleItem item22 = new InvoiceExampleItem()
        {
            Name = "item2"
        };

        InvoiceExampleItem item23 = new InvoiceExampleItem()
        {
            Name = "item3"
        };
        items.list.Add(item21);
        items.list.Add(item22);
        items.list.Add(item23);

        Invoice mockInvoice = new Invoice()
            {
                items = items
        };
            /**/
            XmlDocument mockInvoiceSerialized = SerializeInvoice(mockInvoice);

            XmlDocument mockResponseXml = GenerateXmlResponse(mockInvoiceSerialized);

        GetInvoiceResponse test = new GetInvoiceResponse();
            GetInvoiceResponse response = DeserializeXML<GetInvoiceResponse>(mockResponseXml.OuterXml);

            Invoice resultInvoice = response.Invoice;
        mockResponseXml.Save("C:\\Users\\360Imprimir\\Desktop\\mockResponseXml");
        mockInvoiceSerialized.Save("C:\\Users\\360Imprimir\\Desktop\\mockInvoiceSerialized.Xml");
        if (mockInvoice.items.list.Count != resultInvoice.items.list.Count)
            {
                throw new Exception("wrong number of items");
            }

        }

        public XmlDocument SerializeInvoice(Invoice invoiceToSerialize)
        {
            XmlDocument toReturn = new XmlDocument();
            XmlSerializer serializer = new XmlSerializer(typeof(Invoice));
            XmlReaderSettings settings = new XmlReaderSettings();

            XmlDocument itemsDocument = GetTemplateXML();

            MemoryStream memStm = null, tempStream = null;
            try
            {
                memStm = tempStream = new MemoryStream();

                using (StreamWriter sw = new StreamWriter(memStm, Encoding.UTF8))
                {
                    // Serialize object into raw xml
                    memStm = null;
                    serializer.Serialize(sw, invoiceToSerialize);
                    sw.Flush();

                    // parse raw xml into Xml document
                    tempStream.Position = 0;
                    settings.IgnoreWhitespace = true;
                    var xtr = XmlReader.Create(tempStream, settings);

                    toReturn.Load(xtr);
                }
            }
            finally
            {
                if (memStm != null)
                {
                    memStm.Dispose();
                }
            }

            return toReturn;
        }

        public static T DeserializeXML<T>(string responseString)
            where T : class
        {
            XmlSerializer xmlSerializer = new XmlSerializer(typeof(T));
            using (StringReader sr = new StringReader(responseString))
            {
                return (T)xmlSerializer.Deserialize(sr);
            }
        }

        public XmlDocument GetTemplateXML()
        {
            XmlDocument toReturn = new XmlDocument();
            var decl = toReturn.CreateXmlDeclaration("1.0", "utf-8", string.Empty);
            toReturn.AppendChild(decl);

            return toReturn;
        }

        private XmlDocument GenerateXmlResponse(XmlDocument innerXMLDocument)
        {
            XmlDocument toReturn = GetTemplateXML();

            XmlElement requestElement = toReturn.CreateElement("response");
            requestElement.SetAttribute("status", "success");
            requestElement.InnerXml = innerXMLDocument.DocumentElement.OuterXml;
            toReturn.AppendChild(requestElement);

            return toReturn;
        }

        /// <summary>
        /// Web response from creating a invoice
        /// </summary>
        [System.Xml.Serialization.XmlTypeAttribute(Namespace = "")]
        [System.Xml.Serialization.XmlRootAttribute(Namespace = "", ElementName = "response")]
        public class GetInvoiceResponse
        {
            /// <summary>
            /// Gets or sets the response status
            /// </summary>
            /// <value>
            /// reponse Status
            /// </value>
            [XmlAttribute("status")]
            public string ResponseStatus { get; set; }

            /// <summary>
            /// Gets or sets the new invoice id
            /// </summary>
            /// <value>
            /// generated by invoicera for this response
            /// </value>
            [XmlElement(ElementName = "invoice")]
            public Invoice Invoice { get; set; }
        }
    }
User23145
  • 11
  • 4