Tom W
Tom W

Reputation: 5413

XslCompiledTransform ignores ordering of XPathNodeIterator

I have an XSLT stylesheet that consumes a document and outputs a SOAP message, where the body is in a specific format defined by a WCF data contract (not specified here). The issue is that WCF has a peculiar notion of what constitutes 'alphabetical' ordering and considers the following order to be correct:

This is because it uses ordinal string comparison internally. The details are not interesting, suffice to say that XSLT <sort> doesn't natively support this ordering but in order to transform an input document whose format may vary, into an acceptable SOAP message, the stylesheet must be able to order the output elements according to this peculiar ordering. I've therefore decided to implement node sorting in a script block. This is part of a C# solution and uses XslCompiledTransform and therefore msxsl:script is available.

Given a stylesheet:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:fn="urn:functions"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl exsl"
                xmlns:exsl="http://exslt.org/common"
>
  <msxsl:script implements-prefix="fn" language="C#">
    <![CDATA[

      public class OrdinalComparer : IComparer
      {
          public int Compare(object x, object y)
          {
              return string.CompareOrdinal((string)x, (string)y);
          }
      }

      public XPathNodeIterator OrdinalSort(XPathNavigator source)
      {
        var query = source.Compile("/*");
        query.AddSort(source.Compile("local-name()"), new OrdinalComparer());
        return source.Select(query);
      }    
    ]]>
  </msxsl:script>

  <xsl:template match="Stuff">
    <xsl:element name="Body">
      <xsl:element name="Request">
        <xsl:variable name="sort">
          <xsl:apply-templates select="*"/>
        </xsl:variable>
        <xsl:for-each select="fn:OrdinalSort($sort)">
          <xsl:copy-of select="."/>
        </xsl:for-each>
      </xsl:element>
    </xsl:element>
  </xsl:template>

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

And an input document:

<?xml version='1.0' encoding='utf-8'?>
<Root>
  <Stuff>
    <Age></Age>
    <AIS></AIS>
    <Something></Something>
    <BMI></BMI>
  </Stuff>
</Root>

I would expect the output to order the innermost elements as follows:

This does not happen. Instead the elements are emitted in the order they went in. Debugging into the stylesheet as it executes I can see the OrdinalSort function is called, and the iterator it returns does enumerate the elements in the desired order, but the XSLT processor somehow ignores this and emits the elements in the order they were encountered.

I have verified additionally that parsing the document in a console application and running the same iterator query emits elements in the right order.

Why, and what can I do about it? The only hunch I have at the moment is that the XSLT engine is interpreting the parent Navigator of the iterator (which hasn't changed from what was passed into the sort function) as the element to reproduce, and ignoring the contents of the iterator.

Upvotes: 1

Views: 188

Answers (2)

Tom W
Tom W

Reputation: 5413

I have devised a hideous workaround to the original problem - which was to make XSLT support character ordinal sorting. I consider this an answer, but definitely not a good one. The following snippet illustrates this solution:

<xsl:template match="Stuff">
    <xsl:element name="Body">
      <xsl:element name="Request">

        <xsl:variable name="source">
          <xsl:apply-templates select="*"/>
        </xsl:variable>

        <xsl:for-each select="exsl:node-set($source)/*">
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 1, 1))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 2, 2))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 3, 3))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 4, 4))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 5, 5))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 6, 6))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 7, 7))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 8, 8))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 9, 9))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 10, 10))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 11, 11))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 12, 12))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 13, 13))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 14, 14))"/>
          <xsl:sort data-type="number" select="fn:GetOrdinal(substring(local-name(.), 15, 15))"/>
          <xsl:copy-of select="."/>
        </xsl:for-each>
      </xsl:element>
    </xsl:element>
  </xsl:template>

Where the extension function GetOrdinal is as follows:

    public int GetOrdinal(string s)
    {
        return s.Length == 1 ? (char)s[0] : 0;
    }

This is quite frankly shameful, and I would not advocate ever doing anything as shoddy as this. But it works.

Upvotes: 0

Martin Honnen
Martin Honnen

Reputation: 167571

I am not sure how to solve that with XPathNodeIterator or XPathNavigator, I went as far as creating an XPathNavigator[] from the XPathNodeIterator, to avoid any lazy evaluation effects, but somehow I always ended up with the same result as you.

So as an alternative I have written some code using the DOM implementation in the .NET framework to create some new nodes in the right sort order:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:mf="urn:functions"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl exsl mf"
                xmlns:exsl="http://exslt.org/common"
>
  <msxsl:script implements-prefix="mf" language="C#">
    <![CDATA[

      public class OrdinalComparer : IComparer
      {
          public int Compare(object x, object y)
          {
              return string.CompareOrdinal((string)x, (string)y);
          }
      }

      public XPathNavigator OrdinalSort(XPathNavigator source)
      {
        var query = source.Compile("/root/*");
        query.AddSort("local-name()", new OrdinalComparer());
        XPathNodeIterator result = source.Select(query);
        XmlDocument resultDoc = new XmlDocument();
        XmlDocumentFragment frag = resultDoc.CreateDocumentFragment();
        foreach (XPathNavigator item in result)
        {
          frag.AppendChild(resultDoc.ReadNode(item.ReadSubtree()));
        }
        return frag.CreateNavigator();
      }    
    ]]>
  </msxsl:script>

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="Stuff">
    <Body>
      <Request>
        <xsl:variable name="sort-rtf">
          <root>
            <xsl:copy-of select="*"/>
          </root>
        </xsl:variable>
        <xsl:variable name="sort" select="exsl:node-set($sort-rtf)"/>
        <xsl:variable name="sorted" select="mf:OrdinalSort($sort)"/>
        <xsl:copy-of select="$sorted"/>
      </Request>
    </Body>
  </xsl:template>

</xsl:stylesheet>

Using that approach then the result is

<Root>
  <Body><Request><AIS /><Age /><BMI /><Something /></Request></Body>
</Root>

I have slightly streamlined the code to

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns:mf="urn:functions"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt"
                exclude-result-prefixes="msxsl exsl mf"
                xmlns:exsl="http://exslt.org/common"
>
  <msxsl:script implements-prefix="mf" language="C#">
    <![CDATA[

      public class OrdinalComparer : IComparer
      {
          public int Compare(object x, object y)
          {
              return string.CompareOrdinal((string)x, (string)y);
          }
      }

      public XPathNavigator OrdinalSort(XPathNavigator source)
      {
        var query = source.Compile("*");
        query.AddSort("local-name()", new OrdinalComparer());
        XPathNodeIterator result = source.Select(query);
        XmlDocument resultDoc = new XmlDocument();
        XmlDocumentFragment frag = resultDoc.CreateDocumentFragment();
        foreach (XPathNavigator item in result)
        {
          frag.AppendChild(resultDoc.ReadNode(item.ReadSubtree()));
        }
        return frag.CreateNavigator();
      }    
    ]]>
  </msxsl:script>

  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="Stuff">
    <Body>
      <Request>
        <xsl:variable name="sorted" select="mf:OrdinalSort(.)"/>
        <xsl:copy-of select="$sorted"/>
      </Request>
    </Body>
  </xsl:template>

</xsl:stylesheet>

Having to construct XmlNodes in the extension function C# "script" seems like an overhead but I am not sure how to solve it otherwise.

Upvotes: 1

Related Questions