Search code examples
.netxmlxmlserializer

XML values with same name to list


I have the following XML from which I need to map the "DUE" and "RATE" to a list of objects with XmlSerializer. There can be zero to many, and they're always coming as a pair with the same "idx".

<INVOICE ID="4">
    <STATUS>S</STATUS>
    <TOTAL>6230.00</TOTAL>
    <DUE idx="1">14.12.17</DUE>
    <RATE idx="1">6230.00</RATE>
</INVOICE >
<INVOICE ID="5">
    <STATUS>S</STATUS>
    <TOTAL>3270.00</TOTAL>
    <DUE idx="1">30.11.17</DUE>
    <RATE idx="1">1090.00</RATE>
    <DUE idx="2">07.12.17</DUE>
    <RATE idx="2">1090.00</RATE>
    <DUE idx="3">14.12.17</DUE>
    <RATE idx="3">1090.00</RATE>
</INVOICE>

I have the following setup which is working fine without a list of "Rate" and "Due":

[Serializable]
public class UserInvoicesDto
{
    [XmlElement("INVOICE")]
    public List<UserInvoiceDto> Invoices { get; set; }
}

[Serializable, XmlRoot("INVOICE")]
public class UserInvoiceDto
{
    [XmlAttribute("id")]
    public int InvoiceId { get; set; }
    [XmlElement("TOTAL")]
    public string Total { get; set; }
}

And then I have the following class.

[Serializable]
public class InvoicesDueDates
{
    [XmlAttribute("idx")]
    public string Id { get; set; }
    [XmlElement("DUE")]
    public string DueDate { get; set; }
    [XmlElement("RATE")]
    public string Rate { get; set; }
}

Is it somehow possible?


Solution

  • If you only need to deserialize, you can use do so using XmlSerializer to the following types:

    [XmlRoot(ElementName = "DUE")]
    public class DueDTO
    {
        [XmlAttribute(AttributeName = "idx")]
        public string Idx { get; set; }
        [XmlText]
        public string Text { get; set; }
    }
    
    [XmlRoot(ElementName = "RATE")]
    public class RateDTO
    {
        [XmlAttribute(AttributeName = "idx")]
        public string Idx { get; set; }
        [XmlText]
        public decimal Text { get; set; }
    }
    
    [XmlRoot(ElementName = "INVOICE")]
    public partial class InvoicesDTO
    {
        [XmlAttribute(AttributeName = "ID")]
        public string Id { get; set; }
        [XmlElement(ElementName = "STATUS")]
        public string Status { get; set; }
        [XmlElement(ElementName = "TOTAL")]
        public decimal Total { get; set; }
    
        [XmlElement(ElementName = "DUE")]
        public List<DueDTO> Due { get; set; }
        [XmlElement(ElementName = "RATE")]
        public List<RateDTO> Rate { get; set; }
    }
    

    Then, to combine the Rate and Due list into a single InvoicesDueDates collection, you can use LINQ, e.g. as follows:

    public partial class InvoicesDTO
    {
        public InvoicesDueDates[] InvoicesDueDates
        {
            get
            {
                // To make suure we handle cases where only a Rate or Due item of a specific index is present,
                // perform left outer joins with all indices on both Rate and Due.
                // https://learn.microsoft.com/en-us/dotnet/csharp/linq/perform-left-outer-joins
                var query = from i in Due.Select(d => d.Idx).Concat(Rate.Select(r => r.Idx)).Distinct()
                            join due in Due on i equals due.Idx into dueGroup
                            // Throw an exception if we have more than one due item for a given index
                            let due = dueGroup.SingleOrDefault()
                            join rate in Rate on i equals rate.Idx into rateGroup
                            // Throw an exception if we have more than one rate item for a given index
                            let rate = rateGroup.SingleOrDefault()
                            select new InvoicesDueDates { Id = i, DueDate = due == null ? null : due.Text, Rate = rate == null ? (decimal?)null : rate.Text };
                return query.ToArray();
            }
        }
    }
    
    public class InvoicesDueDates
    {
        public string Id { get; set; }
        public string DueDate { get; set; }
        public decimal? Rate { get; set; }
    }
    

    Notes:

    • This solution takes advantage of the fact that, when XmlSerializer is deserializing a List<T> property and encounters list elements interleaved with other elements, it will append each list element encountered to the growing list.

    • If you re-serialize an InvoicesDTO the result will look like:

        <INVOICE ID="5">
          <STATUS>S</STATUS>
          <TOTAL>3270.00</TOTAL>
          <DUE idx="1">30.11.17</DUE>
          <DUE idx="2">07.12.17</DUE>
          <DUE idx="3">14.12.17</DUE>
          <RATE idx="1">1090.00</RATE>
          <RATE idx="2">1090.00</RATE>
          <RATE idx="3">1090.00</RATE>
        </INVOICE>
      

      Notice that all the information has been retained and re-serialized but the <RATE> and <DUE> sequences have been separated out.

    • If you need to re-serialize with interleaved <RATE> and <DUE> elements, you will have to adopt a different strategy, such as the ones from serializing a list of KeyValuePair to XML or Xml Sequence deserialization with RestSharp.

    • I auto-generated the DTO classes using https://xmltocsharp.azurewebsites.net/ then modified them to fit my naming contentions.

    Sample working .Net fiddle.