1

I'm currently tasked with sending data to a web service that has a very odd way of specifying lists. I am not in control of the schema, and my attempts to make the other party change the schema have failed. So I'm pretty much stuck with this.

The way their schema is defined is this (only the relevant bit is included):

<xs:element name="Part">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="List">
        <xs:complexType>
          <xs:sequence maxOccurs="4">
            <xs:element name="Name" type="xs:string" />
            <xs:element name="Data" type="xs:string" />
            <xs:element name="OtherData" type="xs:string" />
          </xs:sequence>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>
</xs:element>

I used xsd.exe to generate a C# class to serialize the structure easily. The generated bit is as follows:

[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.0.30319.33440")]
[System.SerializableAttribute()]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType=true, Namespace="the namespace")]
public partial class PartList {
    [System.Xml.Serialization.XmlElementAttribute("Name", Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string[] Name { get; set; }

    [System.Xml.Serialization.XmlElementAttribute("Data", Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string[] Data { get; set; }

    [System.Xml.Serialization.XmlElementAttribute("OtherData", Form=System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public string[] OtherData  { get; set; }
}

Yes, parallel arrays. Now, according to their documents (and anecdotal data from another party that generates the XML through other means), the correct/expected xml should look like this (for example, a list with two items - comments inline added for illustration purposes):

<Part xmlns="">
    <List>
        <Name>Some Test</Name> <!-- first item -->
        <Data>123131313</Data> <!-- first item -->        
        <OtherData>0.11</OtherData> <!-- first item -->        
        <Name>Other Lama</Name> <!-- second item -->
        <Data>331331313</Data> <!-- second item -->
        <OtherData>0.02</OtherData> <!-- second item -->
    </List>
</Part>

However, my autogenerated C# class serializes to:

<Part xmlns="">
    <List>
        <Name>Marcos Test</Name> <!-- first item -->
        <Name>Pepe Lama</Name> <!-- second item -->
        <Data>123131313</Data> <!-- first item -->
        <Data>331331313</Data> <!-- second item -->
        <OtherData>0.11</OtherData> <!-- first item -->
        <OtherData>0.02</OtherData> <!-- second item -->
    </List>
</Part>

My XML fails validation against the schema because of the ordering of the items. I'm serializing the class using System.Xml.Serialization.XmlSerializer with the default options. I usually don't have any trouble serializing reasonable schemas for other web services. But for some reason I just can't for the life of me figure out how to do this (if it's even possible).

Any ideas? I already tried using the XmlOrderAttribute, but didn't make a difference in the order of the result.

enriquein
  • 1,048
  • 1
  • 12
  • 28
  • 1
    Your problem is very similar to the problem from [serializing a list of KeyValuePair to XML](https://stackoverflow.com/a/30443169/3744182) or [Xml Sequence deserialization with RestSharp](https://stackoverflow.com/a/32885108/3744182). Both have answers showing two different ways to deserialize a sequence of paired elements using `XmlSerializer`. Are those answers sufficient, or do you need more help? – dbc Dec 04 '17 at 22:39
  • If it were one list I would do it in a heartbeat. But there are a lot of them throughout the schema (it's a 5000 line schema file). I'm hoping there's an easier way to implement this. In any case, if this is the only way to go, I would suggest you post it as an answer and I'll mark you. And thank you! – enriquein Dec 04 '17 at 22:59
  • @dbc Post it as an answer so I can accept it. I will be going with this approach for serialization. The issue of applying this to machine generated code was solved using partial classes and XmlOverrides for the XmlSerializer. Thank you very much for leading me to the solution. – enriquein Dec 06 '17 at 03:27
  • Which one did you choose - the polymorphic array solution from [serializing a list of KeyValuePair to XML](https://stackoverflow.com/q/30442730/3744182) or the `[XmlAnyElement]` solution from [Xml Sequence deserialization with RestSharp](https://stackoverflow.com/a/32885108/3744182)? – dbc Dec 06 '17 at 03:31
  • 1
    @dbc I went with the second one because it was easier to implement. – enriquein Dec 06 '17 at 21:29

2 Answers2

1

The basic problem here is that XmlSerializer recursively descends the object graph and maps objects to blocks of XML element(s), but you want to interleave the elements generated by certain objects, namely the public string[] properties. Unfortunately, this isn't implemented out of the box using only XML serializer attributes.

However, you can generate the XML you need by introducing a surrogate property that can be serialized in the required format. There are two different ways to do this, as shown in the following two questions:

  1. Serializing a list of KeyValuePair to XML shows how to use the polymorphic list functionality of XmlSerializer to generate an interleaved list of elements from multiple collections.

  2. Xml Sequence deserialization with RestSharp shows how to use an [XmlAnyElement] property to generate an interleaved list of elements.

For instance, here is an implementation of approach #1:

public partial class PartList
{
    [XmlIgnore]
    public List<string> Name { get; } = new List<string>();

    [XmlIgnore]
    public List<string> Data { get; } = new List<string>();

    [XmlIgnore]
    public List<string> OtherData { get; } = new List<string>();

    [System.Xml.Serialization.XmlElementAttribute("Name", typeof(Name), Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    [System.Xml.Serialization.XmlElementAttribute("Data", typeof(Data), Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    [System.Xml.Serialization.XmlElementAttribute("OtherData", typeof(OtherData), Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]
    public ValueWrapper<string>[] Values
    {
        get
        {
            var list = new List<ValueWrapper<string>>();
            for (int i = 0, count = Math.Max(Name.Count, Math.Max(Data.Count, OtherData.Count)); i < count; i++)
            {
                if (i < Name.Count)
                    list.Add(new Name { Value = Name[i] });
                if (i < Data.Count)
                    list.Add(new Data { Value = Data[i] });
                if (i < OtherData.Count)
                    list.Add(new OtherData { Value = OtherData[i] });
            }
            return list.ToArray();
        }
        set
        {
            if (value == null)
                return;
            Name.AddRange(value.OfType<Name>().Select(v => v.Value));
            Data.AddRange(value.OfType<Data>().Select(v => v.Value));
            OtherData.AddRange(value.OfType<OtherData>().Select(v => v.Value));
        }
    }
}

public class Name : ValueWrapper<string> { }

public class Data : ValueWrapper<string> { }

public class OtherData : ValueWrapper<string> { }

public abstract class ValueWrapper<T> : ValueWrapper where T : IConvertible
{
    public override object GetValue() => Value; 

    [XmlText]
    public T Value { get; set; }
}

public abstract class ValueWrapper
{
    public abstract object GetValue();
}

Notes:

  • The original collections are marked with [XmlIgnore].

  • A surrogate polymorphic array property ValueWrapper<string>[] Values is introduced that can contain multiple types, one for each possible element name.

  • In creating and returning the array the values from the three collections are interleaved. In the array setter, the values are split by type and directed to the relevant collections.

Sample fiddle.

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

I marked dbc's answer as correct because his comments and answer were what led me to the final implementation. For completeness sake (and to have some sort of documentation just in case I run into this in the future), this is how I ended up implementing this functionality.

I took advantage of the fact that xsd.exe generates all classes as partial classes. That made it easier for me to add the new list/collection properties that will end up being serialized, and not lose changes whenever the schema changes. For example, to override the List property of the RemoteServiceTypePart1 class:

public partial class RemoteServiceTypePart1
{
    // Tell the Xml serializer to use "List" instead of "List_Override" 
    // as the element name.  
    [XmlAnyElement("List")]
    public XElement List_Override
    {
        get {
            var result = new List<XElement>(); 

            for (int i = 0; i < List.Name.Length; i++)
            {
                result.Add(new XElement("Name", List.Name[i]));
                result.Add(new XElement("Data", List.Data[i]));
                result.Add(new XElement("OtherData", List.OtherData[i]));
            }

            return new XElement("List", result.ToArray());
        }

        set { }
    }
}

Now the only remaining issue is how to add the [XmlIgnore] attribute to the original property, and not have to add the attribute by hand every time the schema changes. To do that, I used the XmlAttributeOverrides class and placed it where the xml serialization logic is:

var xmlIgnoreAttr = new XmlAttributes { XmlIgnore = true };

var overrides = new XmlAttributeOverrides();
// Add an override for each class that requires properties to be ignored.
overrides.Add(typeof(RemoteServiceTypePart1), "List", xmlIgnoreAttr);
overrides.Add(typeof(RemoteServiceTypePart2), "List", xmlIgnoreAttr);
overrides.Add(typeof(RemoteServiceTypePart3), "List", xmlIgnoreAttr);

// In a real-world implementation, you will need to cache this object to 
// avoid a memory leak. Read: https://stackoverflow.com/a/23897411/3744182
// Thanks to dbc for letting me know in the comments.
var ser = new XmlSerializer(typeof(RemoteServiceType), overrides);
// serialize, send xml, do whatever afterwards

That's it. Now the output XML looks like this:

<RemoteServiceTypePart1 xmlns="">
  <List>
    <Name>Marcos Test</Name>
    <Data>123131313</Data>
    <OtherData>0.11</OtherData>
    <Name>Pepe Lama</Name>
    <Data>331331313</Data>
    <OtherData>0.02</OtherData>
  </List>
</RemoteServiceTypePart1>
enriquein
  • 1,048
  • 1
  • 12
  • 28
  • 1
    Note - as explained in [this answer](https://stackoverflow.com/a/23897411/3744182), to avoid a memory leak, an `XmlSerializer` constructed with a non-default constructor should be statically cached and reused. See also [the docs](https://msdn.microsoft.com/en-us/library/system.xml.serialization.xmlserializer.aspx#Remarks) which state *If you use any of the other constructors, multiple versions of the same assembly are generated and never unloaded, which results in a memory leak and poor performance. ... Otherwise, you must cache the assemblies in a Hashtable,* – dbc Dec 06 '17 at 22:18
  • That's a great catch. Thanks again! – enriquein Dec 06 '17 at 22:38