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.
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:
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.
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.