问题
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.
回答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:
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.
回答2:
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>
来源:https://stackoverflow.com/questions/47641622/serialize-parallel-arrays-to-xml-in-c-sharp-in-a-specific-order