1

One use case for interfacing with a legacy WCF is to hide all the .NET service code behind a more modern RESTful API to which we Post the (seemingly correct) XML, and deserialize it to the WCF class we need require.

A common problem is deserialization errors due to XML elements being in the wrong order. The most subtle of which is Out of order elements being silently discarded during deserialization, and it's resulting sub Class or Property value being null.

We require a viable solution that doesn't involve the risky removal of Order attributes or other manipulation of the service Classes because it could have unintended consequences for the remainder of the WCF service.

I tried to use reflection to generically create this ordered list for a given Class. It appeared to work but Class inheritance causes parent elements to appear in the wrong order.

golfalot
  • 956
  • 12
  • 22
  • This seems really useful, but a Stack Overflow post must be either a question or an answer, so I would suggest separating out everything after *Below is the method that I settled on* into a [self answer](https://stackoverflow.com/help/self-answer) and rewriting the started to be more or less in the form of a question. – dbc Aug 24 '23 at 20:03
  • 1
    The step and solution are well organized. However, SO is a q&a platform. It's even better to throw out a question and change the content you post as an answer. – Jiayao Aug 25 '23 at 03:07
  • Hopefully I've appeased the purists with Q&A format re-edit. Thanks for the constructive and positive feedback. – golfalot Aug 31 '23 at 17:00

1 Answers1

1

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;
            }));
        }
golfalot
  • 956
  • 12
  • 22