Below is the method that I settled on - it's a bit onerous but you'll only have to do it once for each data Class and it's highly transparent when troubleshooting.
The aim of the game is to sort the Posted XML before deserializing, using a reference order for the elements / XPaths.
Step 1 probably already done if you found this article
- Use with the
myservicename.wsdl
file to generate all the service reference Classes using DataContractSerializer
as the preferred serializer.
- Figure out which top level data Class you want to deserialize your XML to.
Step 2 schema extraction
- Open the WCF
myservicename.wsdl
in an XML editor and Locate the schema which contains the data Class definition note that you need to search for it by it's DataContractAttribute Name
- save the schema to file, and do the same for any child element schemas (I had 6 to contend with!)
Step 3 Generate sample Xml File (include every possible element)
- I used
Altova XmlSpy
for this but there are other options
- With the schema open, Validate the schema. This will likely involve jumping through a few hoops such as extracting child schemas / namespaces from the
wsdl
and saving them, adding them to the Class schema with xs:import
and setting schemaLocation
to file paths
- Do not proceed further until you can validate the schema!
- DATA/Schema menu -> Generate sample Xml File
- It will prompt to ask element to use as root for the sample - this is where you choose your Class
- Make certain the generated sample passes schema validation
Step 4 In C# prepare for Xml sorting. Convert the sample XML to an XPath + order dictionary
internal class SampleXmlHelpers
{
/// <summary>
/// Use a model sample Xml to obtain the order/sequencing of elements that comply with a given Xsd
/// This will be sorted later to sort incoming Xml before deserializing
/// </summary>
/// <returns></returns>
public static Dictionary<string, int> GetXmlXPathOrderDic(Type classType)
{
const string subFolder = "Schemas";
var binDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var baseDirectory = Path.GetFullPath(Path.Combine(binDirectory, subFolder));
var xmlFileName = string.Empty;
if (classType.Name == "HhClaimAddRequestType")
{
xmlFileName = "HhClaimAddRequestType-XmlSpy-Sample.xml";
}
else if (classType.Name == "HhClaimUpdateRequestType")
{
xmlFileName = "HhClaimUpdateRequestType-XmlSpy-Sample.xml";
}
else
{
throw new ArgumentOutOfRangeException(nameof(classType));
}
string xml = File.ReadAllText(Path.Combine(baseDirectory, xmlFileName));
var doc = new XmlDocument();
//doc.LoadXml(Path.Combine(baseDirectory, xmlFileName));
doc.LoadXml(xml);
var dic = ExtractXPathsAndSequence(doc);
return dic;
}
private static Dictionary<string, int> ExtractXPathsAndSequence(XmlDocument document)
{
Dictionary<string, int> xpaths = new Dictionary<string, int>();
int sequenceNumber = 0; // Initial sequence number
// Recursive function to traverse the XML nodes
void Traverse(XmlNode node, string currentPath)
{
if (node.NodeType == XmlNodeType.Element)
{
int index = GetNodePosition(node);
//string newPath = $"{currentPath}/{node.Name}[{index}]";
string newPath = $"{currentPath}/{node.Name}";
if (!xpaths.ContainsKey(newPath))
{
xpaths[newPath] = sequenceNumber++;
}
// Recurse for child nodes
foreach (XmlNode childNode in node.ChildNodes)
{
Traverse(childNode, newPath);
}
}
}
Traverse(document.DocumentElement, "");
return xpaths;
}
// Helper function to get the position of a node among its siblings of the same name
private static int GetNodePosition(XmlNode node)
{
if (node.ParentNode == null)
{
return 1;
}
int index = 1; // XPath index starts from 1
foreach (XmlNode sibling in node.ParentNode.ChildNodes)
{
if (sibling == node)
{
return index;
}
if (sibling.Name == node.Name)
{
index++;
}
}
return -1; // Should not reach here unless there's a problem with the XML structure
}
}
Step5 sort the incoming Posted XML before deserializing
public static string SortXmlElements(string xmlContent, Dictionary<string, int> sortingDic)
{
XDocument xmlDoc = XDocument.Parse(xmlContent);
XDocument sortedXmlDoc = new XDocument(
xmlDoc.Declaration,
SortElementsByXPathOrder(xmlDoc.Root, sortingDic)
);
return sortedXmlDoc.ToString();
}
static XElement SortElementsByXPathOrder(XElement element, Dictionary<string, int> sortingDic)
{
if (!element.HasElements)
{
return new XElement(element.Name, element.Attributes(), element.Value); // Leaf node with its value
}
var sortedElements = element.Elements()
.OrderBy(e => sortingDic[GetSimpleXPathWithNamespacePrefix(e)]) // Sort by Schema order
.Select(e => SortElementsByXPathOrder(e, sortingDic));
XElement newElement = new XElement(
element.Name,
element.Attributes(),
sortedElements
);
// Remove the original nodes after copying them to the sorted element
element.Nodes().Remove();
return newElement;
}
static string GetSimpleXPathWithNamespacePrefix(XElement element)
{
return "/" + string.Join("/", element.AncestorsAndSelf().Reverse().Select(e =>
{
string prefix = e.GetPrefixOfNamespace(e.Name.Namespace);
return !string.IsNullOrEmpty(e.Name.NamespaceName) && !string.IsNullOrEmpty(prefix)
? prefix + ":" + e.Name.LocalName
: e.Name.LocalName;
}));
}