TL;DR: Always use the /order
option when using xsd.exe.
If you have ever generated C# classes from an XML schema, chances are you used xsd.exe for that.
You probably also expected the tool to produce annotated classes that, when fed to XmlSerializer (as Microsoft expects you to), are serialized into XML which is at least structurally valid against the schema from which you created the classes.
Well, that is in fact not the case, at least it is not xsd.exe’s default.
If you generate the classes with
xsd myschema.xsd /classes
from a schema with xs:sequence
definitions in it, you will find that xsd.exe generates the classes in such a way that their members have no explicitly annotated serialization order, but are declared in the order of the corresponding XML elements defined in the sequence.
If you then use XmlSerializer to serialize those classes, chances are that the output is correct in that the XML elements come in the order defined by the schema, and all seems well.
It also seemed well in my current project, until I started writing some unit tests for the mapping from our domain classes to the generated schema classes. We used NUnit.org as testing framework and I made use of TestCaseSource
with some expression magic (see future blog post about fun with collection initialization syntax) to avoid too much duplication.
Then, all of a sudden, our sanity-check integration test was failing because the generated XML no longer validated against the schema.
Some of the XML elements were not in the order defined by the xs:sequence
anymore.
This was surprising as I had touched neither the integration test nor the generated schema classes.
The unit tests I wrote did not even reference XmlSerializer.
They only tested the data mapping onto the generated schema classes.
After finding the breaking commit with git bisect and then doing a manual binary search by commenting out the added test code, I found that the mere existence of one single unit test with a single case in the TestCaseSource
changed the XML generated by the XmlSerializer when the integration test was run, namely, that the order of the XML elements was not the one defined in the schema and reflected in the declaration order of generated class members.
Yes, you read that right: the fact that this one unit test existed, without ever being run, did change the XML serialization in our integration test! WTF?
Furthermore, after some playing around with the test cases, I found that, when running the integration test, the XmlSerializer serialized the properties from the test cases as the XML parent element’s first children, independent of the generated class’s member declaration order.
It was absurd.
I do not know what NUnit.org does during its generation of the TestCaseSource
test cases or what this has to do with XmlSerializer.
But after some digging-around in Microsoft’s reference source for XmlSerializer and its companion classes it looked to me as if the order in which XmlSerializer serializes properties that have no order annotation is in fact undefined.
When XmlSerializer finds no order annotation, all properties get the same internal order (“0” I think).
When the property list is then sorted by order, Array.Sort
is used, which must be expected to use an unstable sorting algorithm according to its documentation (under “Remarks” of every single overload).
This means that after sorting, the order of the properties in the array and thus the serialization order is effectively undefined and could be anything.
So it turned out that XML serialization in our project seemed to just happen to work because of the way the current implementation of XmlSerializer happens to treat unordered properties and the way the current implementation of Array.Sort
happens to be implemented.
The remedy is to simply add the /order
option when using xsd.exe:
xsd myschema.xsd /classes /order
That way all generated class members end up annotated with an explicitly defined order for XML serialization and will be serialized in the order defined in xs:sequence
.
Why this is not xsd.exe’s default is beyond me.
But after this insane witch hunt I will probably never again forget to use the order option.