1. Introduction

On the 15th January 2020 Microsoft announced the release of BizTalk Server 2020. A number of new features have been added to the product (which can be read about here), and two new, related features  are the addition of XSLT 3.0 support and Custom XSLT Transforms. What is now provided is a new extensible model for map execution (at runtime) and out of the box support for the Saxon-HE XSLT 3.0 processor (though other third-party XSLT processors can also be configured). The introduction of XSLT 3.0 in BizTalk Server – which in practice means any XSLT version – opens new XSLT transformation possibilities previously unavailable to the developer.

This article assumes the reader is familiar with the BizTalk Server mapper and the use of both Custom Extensions and Custom XSLT.  

2. Overview

BizTalk Server’s XSL transform engine implementation has, up to now, been based on .Net Framework XSLT Transformations. This implementation was limited to XSLT 1.0, but now a new map property: XSLT Transform Engine can be configured to specify which XSLT processor BizTalk Server should use when executing a map. This new functionality allows XSLT processors which implement newer versions of XSLT to be used. Note that this property is at map level so different maps can use different XSLT processors as shown below:

In-built Engine Options

After installation there are three compiler options available to the developer:

  1. Undefined. This specifies that the compiler is to use the global XSLT transform engine setting and no map specific override is applied. By default, BizTalk uses the .Net Framework XSLT processor as the global engine. However, this can be overridden by adding a new string value XsltEngine in the BizTalk Server registry and setting its value to the AssemblyQualifiedName of the class implementing the transformation processor. This global setting is retrieved from the following registry keys:

    • 64-bit host instances: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\BizTalk Server\3.0\Configuration

    • 32-bit host instances: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\BizTalk Server\3.0\Configuration

    For example, to specify the Saxon HE processor:

  2. .Net Framework. Selecting this option will cause use the .Net Framework XSLT 1.0 processor to execute the map. It will also cause an additional Compiler property Use XSL Transform (previously introduced in BizTalk Server 2013R2) which gives the option to use the XslCompiledTransform class for improved performance in mapping (see here).

     

  3. Saxon 9 HE. This engine doesn't support embedded scripting. As a result, functoids shipped as part of BizTalk Server may not function well with Saxon 9 HE. However, the Custom Extension XML is still a supported way for creating custom extensions for the Saxon 9 HE transform engine. The custom .Net extension functions implement the ExtensionFunction and ExtensionFunctionDefinition interfaces and added into the Custom Extension XML file. The Saxon 9 HE transform engine will register extension functions defined in Custom Extension XML, and the transform processor can then recognize and invoke any call from XSLT.

Custom Engine Options

In addition to the in-built processor options custom processors are also supported. This is achieved by implementing a class derived from a new abstract class ITransform2 (Microsoft.XLANGs.BaseTypes.ITransform2) in assembly Microsoft.XLANGs.BaseTypes.dll. The members to be implemented are given below:

public abstract class ITransform2
{
   // This is not required, user can implement if they want their transform support custom extension.
   // These 3 parameters passed in are from "Custom Extension XML", in which user can provide namespace, assembly name, class name of the extension object,
   //here user should create the extension behavior, like extension object creation, and registry.
    public virtual void RegisterExtension(string namespaceUri, string assemblyName, string className);
            
    // Load XSLT string.
    public abstract void Load(string xslt);
 
    // Transform input stream into out string.
    // Notice BizTalk actually doesn't support xslt arguments for now, it is reserved for future usage.
    public abstract void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results);
}
The compiled DLL file is then copied to the Transform Components folder in the installation path (e.g. C:\Program Files (x86)\Microsoft BizTalk Server\Transform Components) on every BizTalk runtime machine.        
       

To use the custom transform engine in the Visual Studio developer tools, the CustomTransform.xml file, located in the "Developer Tools" folder (e.g. c:\Program Files (x86)\Microsoft BizTalk Server\Developer Tools\CustomTransform.xml), needs to be updated with new transform engine details, as shown below (Visual Studio will need to be restarted to pickup the changes):

<?xml version="1.0" encoding="utf-8" ?>
<CustomTransforms>
  <Transform
    DisplayName="Saxon 9 HE"
    TypeAssemblyQualifiedName="Microsoft.XLANGs.BaseTypes.SaxonHEXsltTransform, Microsoft.XLANGs.BaseTypes, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
  <Transform
    DisplayName="Saxon 9 HE (Debug)"
    TypeAssemblyQualifiedName="CustomTransform.SaxonHEXsltTransform, CustomTransform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f14d083427ac03c4, processorArchitecture=MSIL"/>
  <Transform
    DisplayName="XmlPrime"
    TypeAssemblyQualifiedName="CustomTransform.XmlPrimeTransform, CustomTransform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f14d083427ac03c4, processorArchitecture=MSIL"/>
  <Transform
    DisplayName=".Net Framework XSLT 2.0"
    TypeAssemblyQualifiedName="CustomTransform.DNFXsltTransform, CustomTransform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f14d083427ac03c4, processorArchitecture=MSIL"/>
</CustomTransforms>
 

The DisplayName attribute is the text displayed in the drop-down list for XSLT transform engine grid property. The TypeAssemblyQualifiedName attribute is the fully qualified name of the class that implements the custom engine. For more details see XSLT Transform Engine (Grid Property)

3. Using the Saxon 9 HE Engine

As already mentioned, when Saxon 9 HE is selected as the XSLT Transform Engine the mapper will use this engine to perform the transform. From the BizTalk Server Mapper perspective there are essentially three key questions when using the Saxon 9 HE processors:

  1. How can XSLT 3.0 be used in the map;

  2. What built-in functoids can and can’t be used within the map;

  3. How can Custom Extensions be called from map?

 

XSLT 3.0 Script

The first, and obvious, answer to using XSLT 3.0 is to use a custom XSLT file containing the XSLT script. This is done by configuring the Custom XSLT Path grid property, in this ‘mode’ the mapper ignores any functoids and only executes the external XSLT stylesheet. It is also worth pointing out that, unlike with the .NET Framework engine, the xsl:import (and xsl:include) element can be used to import other XSLT files.

It is possible to use XSLT 3.0 with the Scripting functoid via the Inline XSLT and Inline XSLT Call Template script types. One interesting point to note is that the Inline XSLT Call Template does not necessarily need to contain an xsl:template element. There is no validation and so, for example, it is possible to add an xsl:function instead and have this function called from another Inline XSLT or Inline XSLT Call Template script type.

One further point, the XSLT 3.0 specification defines a new element xsl:evaluate which allows the dynamic evaluation of an XPath expression. However, while the Saxon 9 HE processor implements this element, it is in a restricted sense, so that while the processor recognises the element as valid XSLT 3.0 the feature is disabled and results in an error if the instruction is executed.

 

Built-in Functoids

There is no issue with direct field mapping, but the Saxon 9 HE engine does not support embedded scripting. This means that most of The BizTalk Server functoids in the Mapper toolbox will not work. The ones that will work are:

  • all the Advanced Functoids except the Scripting Functoid;

  • the Inline XSLT, Inline XSLT Call Template script types of the Scripting Functoid;

  • the isNil Logical Functoid.

There are some advantages to using the BizTalk Server mapper: it provides a good visual aid to non-developers of how source and destinations fields are mapped; it performs validation on the mapping and warns if, say, mandatory destination fields are not mapped; and it provides an alternative to the developer who is not well versed in XSLT. The Inline XSLT Call Template can be used in place of those functoids that cannot be used. For example, the default example script in the Inline XSLT Call Template script type (MyXsltConcatTemplate) already provides a concatenation template. This method may suffice for some maps but obviously the more complex the map the more difficult it becomes to visually understand the mapping.

There is a workaround to using the built-in functoids, though it will involve some XSLT 3.0 coding. All the functoids that cannot be used generate inline C# script, for example, consider the map below:

Validating the map generates an XSLT that contains C# script like this below:

<?xml version="1.0" encoding="UTF-16"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:var="http://schemas.microsoft.com/BizTalk/2003/var" exclude-result-prefixes="msxsl var userCSharp" version="1.0" xmlns:ns0="http://BizTalk_Server_Project1.Schema1" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
  <xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
  <xsl:template match="/">
    <xsl:apply-templates select="/ns0:Root" />
  </xsl:template>
  <xsl:template match="/ns0:Root">
    <xsl:variable name="var:v1" select="userCSharp:StringConcat(string(Field01/text()) , string(Field02/text()))" />
    <xsl:variable name="var:v2" select="userCSharp:MathAdd(string(Field03/text()) , string(Field04/text()))" />
    <ns0:Root>
      <Field01>
        <xsl:value-of select="$var:v1" />
      </Field01>
      <Field02>
        <xsl:value-of select="$var:v2" />
      </Field02>
    </ns0:Root>
  </xsl:template>
  <msxsl:script language="C#" implements-prefix="userCSharp"><![CDATA[
public string StringConcat(string param0, string param1)
{
    ...
}
 
public string MathAdd(string param0, string param1)
{
    ...
}
 
]]></msxsl:script>
</xsl:stylesheet>
But any attempt to execute the map (using the Saxon 9 HE engine) will result in the error “Cannot find a 2-argument function named Q{http://schemas.microsoft.com/BizTalk/2003/userCSharp}StringConcat()”. This is obviously because Saxon 9 HE knows nothing about the proprietary msxsl:script element. Instead, what Saxon 9 HE is searching for is an xsl:function that corresponds with the namespace/function name within the XSLT. Recalling from the previous section that we can add xsl:function to an Inline XSLT Call Template, if we develop functions with the same signature as the inline C# scripts they will get executed instead. In other words, by adding a scripting functoid (with no input or output links) and adding the inline script below to an Inline XSLT Call Template the map example given above will successfully execute:
        

<xsl:function name="userCSharp:StringConcat" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xsl:param name="param1" as="xs:string" />
  <xsl:param name="param2" as="xs:string" />
  <xsl:value-of select="concat($param1, $param2)" />
</xsl:function>
 
<xsl:function name="userCSharp:MathAdd" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xsl:param name="param1" as="xs:string" />
  <xsl:param name="param2" as="xs:string" />
  <xsl:value-of select="number($param1) + number($param2)" />
</xsl:function>
 

There are few things to note:

  • Because the BizTalk Mapper puts a string() function around each parameter the xsl:function parameters must be declared as xs:string and any conversion take place with the function (as in the userCSharp:MathAdd function);

  • External Assembly must be set to a lower precedence than Inline C# in the Script Type Precedence grid property;

  • It is possible to add the functions to an external file and imported using the xsl:import element (see Appendix A). This is because in XSLT 3.0 it is no longer a requirement for this element to appear before all other elements at the top level;

  • Switching the XSLT transform engine property to .Net Framework will result in the error “'xsl:function' cannot be a child of the 'xsl:stylesheet' element.” if the map is run because the xsl:function element does not exist in XSLT 1.0;

The BizTalk Server mapper will always create an XSLT that has a version attribute set to ‘1.0’. This means that any custom XSLT 2.0/3.0 processor - assuming it supports version 1.0 by implementing a backwards compatibility mode – may process differently than if the version is set to 2.0 or higher.

 

Custom Extensions

Custom extensions via the use of the Custom Extension XML property is still supported for the for Saxon 9 HE processor, though its implementation (compared to the .NET framework) is different from .NET. The extension functions are done by implementing the ExtensionFunctionDefinition and ExtentionFunctionCall interfaces. An example is given below, the extension calculates the square-root of a number passed as an argument:

using System;
using System.Collections.Generic;
using Saxon.Api;
 
namespace CSharpExtensions
{
    /// <summary>
    /// Example extension function to compute a square root.
    /// </summary>
 
    public class Sqrt : ExtensionFunctionDefinition
    {
        public override QName FunctionName
        {
            get
            {
                return new QName("http://schemas.microsoft.com/BizTalk/2003/ScriptNS0", "sqrt");
            }
        }
 
        public override int MinimumNumberOfArguments
        {
            get
            {
                return 1;
            }
        }
 
        public override int MaximumNumberOfArguments
        {
            get
            {
                return 1;
            }
        }
 
        public override XdmSequenceType[] ArgumentTypes
        {
            get
            {
                return new XdmSequenceType[]{
                    new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?')
                };
            }
        }
 
        public override XdmSequenceType ResultType(XdmSequenceType[] ArgumentTypes)
        {
            return new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?');
        }
 
        public override bool TrustResultType
        {
            get
            {
                return true;
            }
        }
 
 
        public override ExtensionFunctionCall MakeFunctionCall()
        {
            return new SqrtCall();
        }
    }
 
    internal class SqrtCall : ExtensionFunctionCall
    {
        public override IEnumerator<XdmItem> Call(IEnumerator<XdmItem>[] arguments, DynamicContext context)
        {
            Boolean exists = arguments[0].MoveNext();
            if (exists)
            {
                XdmAtomicValue arg = (XdmAtomicValue)arguments[0].Current;
                double val = (double)arg.Value;
                double sqrt = System.Math.Sqrt(val);
                XdmAtomicValue result = new XdmAtomicValue(sqrt);
                return (IEnumerator<XdmItem>)result.GetEnumerator();
            }
            else
            {
                return EmptyEnumerator<XdmItem>.INSTANCE;
            }
        }
    }
}
 
The custom Extensions XML file contains the following: 
       

<ExtensionObjects>
    <ExtensionObject
       AssemblyName="CSharpExtensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx, processorArchitecture=MSIL"
       ClassName="CSharpExtensions.Sqrt" />
</ExtensionObjects>
 

There are a couple of ways to call the extension. The first is from within a custom XSLT file:

<?xml version="1.0" encoding="utf-16"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:var="http://schemas.microsoft.com/BizTalk/2003/var" exclude-result-prefixes="msxsl var ScriptNS0" version="1.0" xmlns:ns0="http://BizTalk_Server_Project1.Schema1" xmlns:ScriptNS0="http://schemas.microsoft.com/BizTalk/2003/ScriptNS0">
    <xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
    <xsl:template match="/">
        <xsl:apply-templates select="/ns0:Root" />
    </xsl:template>
    <xsl:template match="/ns0:Root">
        <ns0:Root>
            <xsl:variable name="var:v1" select="ScriptNS0:sqrt(64)" />
            <Field01>
                <xsl:value-of select="$var:v1" />
            </Field01>
        </ns0:Root>
    </xsl:template>
</xsl:stylesheet>
 
The extension can be called from a Scripting functoid Inline XSLT or XSLT Template. The important thing to note is to ensure the correct namespace prefixes are declared:        

 

It is also possible to call the extension from a Scripting functoid External Assembly script type, but a couple of changes are first required to the extension C# code. The way the .Net Framework and Saxon HE makes use of custom extensions differ. The former is expecting a class method (with any method parameters) to call whereas the latter requires the class name (with any constructor parameters). The solution, then, is to add an empty method to the class that matches how the class in called from the XSLT, for example:

        public void sqrt(string param1)

        {

        }

In addition, because the BizTalk Server mapper puts a string() function around each parameter, it is necessary to configure each parameter as a xs:string value (i.e. set the data type to QName.XS_STRING) and convert to the correct data type with the function itself. The new class in given in Appendix B and can be specified as the method to call in the External Assembly, as shown below:

 

This ensures the correct call to instantiate the class is generated in the XSLT as shown below:

    <ns0:Root>

      <xsl:call-template name="sqrt2">

        <xsl:with-param name="param1" select="string(Field01/text())" />

      </xsl:call-template>

      <xsl:variable name="var:v1" select="ScriptNS0:sqrt(string(Field01/text()))" />

      <Field02>

        <xsl:value-of select="$var:v1" />

      </Field02>

    </ns0:Root>

 

4. Using a Custom XSLT Transform Engine

Starting with BizTalk Server 2020 custom XSLT transform engines are supported for the BizTalk Server mapper. In theory, at least, this should mean that any XSLT processor written for the .NET platform ought to be available to the developer. In this section we’ll look at a Custom XSLT transformation implementation for two engines: Saxon HE and .Net Framework. This might see strange as they’re already available out of the box, but it will allow us to address a couple of issues with the existing implementations as well as providing examples on how to develop the Custom XSLT Transforms.

Custom Saxon 9HE Engine

There is an issue when developing with the Saxon 9 HE engine. When testing a map in Visual Studio, often it fails with an error “Errors were reported during stylesheet compilation” reported in the output window. A more detailed description of the error would be helpful. The Saxon 9 HE engine does capture more details on the error(s) but this is not always exposed though to the Visual Studio output window.  The example below implements the Saxon 9 HE engine with tracing that can be captured using DebugView:

public class SaxonHEXsltTransform : ITransform2
{
    protected bool legacyWhitespaceBehavior;
    protected Processor processor;
    protected XsltCompiler compiler;
    protected Xslt30Transformer transformer;
 
    public SaxonHEXsltTransform()
    {
        System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - Constructor");
 
        this.legacyWhitespaceBehavior = Microsoft.BizTalk.ScalableTransformation.BTSXslTransform.LegacyWhitespaceBehavior;
        this.processor = new Processor(true);
 
        this.processor.SetProperty(FeatureKeys.STRIP_WHITESPACE, this.legacyWhitespaceBehavior ? "all" : "none");
        this.compiler = processor.NewXsltCompiler();
        this.compiler.ErrorList = new List<StaticError>();
    }
 
    public override void RegisterExtension(string namespaceUri, string assemblyName, string className)
    {
        System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - RegisterExtension");
        System.Diagnostics.Trace.WriteLine("namespaceUri: " + namespaceUri + "assemblyName: " + assemblyName + "className: " + className);
 
        Assembly assembly = Assembly.Load(assemblyName);
        Object obj = assembly.CreateInstance(className);
 
        ExtensionFunction function = obj as ExtensionFunction;
        if (function != null)
        {
            this.processor.RegisterExtensionFunction(function);
        }
 
        ExtensionFunctionDefinition functionDefinition = obj as ExtensionFunctionDefinition;
        if (functionDefinition != null)
        {
            this.processor.RegisterExtensionFunction(functionDefinition);
        }
 
        if (function == null && functionDefinition == null)
        {
            System.Diagnostics.Trace.WriteLine(string.Format("Invalid extension class {0}, it should be of type {1} or {2}.", className, typeof(ExtensionFunction).Name, typeof(ExtensionFunctionDefinition).Name));
            throw new ArgumentException(string.Format("Invalid extension class {0}, it should be of type {1} or {2}.", className, typeof(ExtensionFunction).Name, typeof(ExtensionFunctionDefinition).Name));
        }
    }
 
    public override void Load(string xslt)
    {
        try
        {
            System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - Load");
            System.Diagnostics.Trace.WriteLine("XSLT: " + xslt);
 
            XsltExecutable executable = this.compiler.Compile(new StringReader(xslt));
            this.transformer = executable.Load30();
        }
        catch (Exception ex)
        {
            System.Diagnostics.Trace.WriteLine("");
            System.Diagnostics.Trace.WriteLine("XSLT Errors");
            System.Diagnostics.Trace.WriteLine("===========");
            foreach (StaticError se in this.compiler.ErrorList)
            {
                System.Diagnostics.Trace.WriteLine("Message: " + se.Message);
                System.Diagnostics.Trace.WriteLine("Column Number: " + se.ColumnNumber.ToString());
                System.Diagnostics.Trace.WriteLine("Line Number: " + se.LineNumber.ToString());
                System.Diagnostics.Trace.WriteLine("Source: " + se.Source);
            }
            System.Diagnostics.Trace.WriteLine("Exception: " + ex.ToString());
        }
    }
 
    public override void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results)
    {
        System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - Transform");
 
        if (xsltArguments != null)
        {
            Dictionary<Saxon.Api.QName, XdmValue> parameters = new Dictionary<Saxon.Api.QName, XdmValue>();
            foreach (XmlQualifiedName name in xsltArguments.Keys)
            {
                parameters[new Saxon.Api.QName(name)] = new XdmExternalObjectValue(xsltArguments[name]);
            }
 
            this.transformer.SetStylesheetParameters(parameters);
        }
 
        this.transformer.InputXmlResolver = new XmlUrlResolver();
        Serializer serializer = processor.NewSerializer();
        serializer.SetOutputStream(results);
        this.transformer.ApplyTemplates(input, serializer);
        serializer.Close();
    }
}

Custom .NET Framework Engine

This next example implements a custom .NET Framework engine. There is an issue with the in-built .NET Framework engine or, specifically, within the BizTalk Server mapper compiler (Microsoft.BizTalk.Mapper.Compiler.XsltCodeManager). When using the BizTalk Server mapper (as opposed to setting the Custom XSLT Path grid property and using a custom XSLT file), the CreateTransformNode method hard-codes the XSLT version (i.e. it sets the attribute: AddAttribute(myXsltTransformNode, "version", "1.0");). Now, while the .NET Framework engine only supports XSLT 1.0, it does support Forward Compatibility. However, this compatibility is only enabled by setting the XSLT version attribute to a value greater than ‘1.0’, a property which the BizTalk Server mapper doesn't expose (note the version grid property is the XML output version no the XSLT version).

We have already experienced this problem in a previous section (Built-in Functoids) where, when changing the XSLT transform engine back to .NET Framework, an error was reported – if Forward Compatibility mode was available to us we should be able to set it and switch between each XSLT transform engine without issue (the xsl:function element would be ignored and no error reported).

The example below implements the .NET Framework engine and changes the XSLT version (note this used the XslCompiledTransform class):

public class DNFXsltTransform : ITransform2
 {
     protected XsltArgumentList xsltArgumentList;
     protected XslCompiledTransform xslCompiledTransform;
     private static readonly System.Xml.Xsl.XsltSettings xsltSettings = new System.Xml.Xsl.XsltSettings(true, true);
 
     public DNFXsltTransform()
     {
         xsltArgumentList = new XsltArgumentList();
         xslCompiledTransform = new XslCompiledTransform();
     }
 
     public override void RegisterExtension(string namespaceUri, string assemblyName, string className)
     {
         Assembly assembly = Assembly.Load(assemblyName);
         object extension = assembly.CreateInstance(className);
         xsltArgumentList.AddExtensionObject(namespaceUri, extension);
     }
 
     public override void Load(string xslt)
     {
         XmlDocument xmlDocument = new XmlDocument();
         xmlDocument.LoadXml(xslt);
 
         // Change the xslt version atttribute to enable Forward Compatibility
         xmlDocument.DocumentElement.RemoveAttribute("version");
         xmlDocument.DocumentElement.SetAttribute("version", "3.0");
 
         xslCompiledTransform.Load(xmlDocument, xsltSettings, new XmlUrlResolver());
     }
 
     public override void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results)
     {
         AddArguments(xsltArgumentList, xsltArguments);
         using (XmlWriter xmlWriter = XmlWriter.Create(results, xslCompiledTransform.OutputSettings))
         {
             xslCompiledTransform.Transform(new XmlTextReader(input), xsltArgumentList, xmlWriter, new XmlUrlResolver());
             xmlWriter.Flush();
         }
     }
 
     private void AddArguments(XsltArgumentList argumentList, IDictionary<XmlQualifiedName, object> xsltArguments)
     {
         if (xsltArguments != null)
         {
             foreach (XmlQualifiedName key in xsltArguments.Keys)
             {
                 if (argumentList.GetParam(key.Name, key.Namespace) != null)
                 {
                     argumentList.RemoveParam(key.Name, key.Namespace);
                 }
                 argumentList.AddParam(key.Name, key.Namespace, xsltArguments[key]);
             }
         }
     }
 }

By using this .NET Framework engine, the BizTalk Server map given in the previous section now executes successfully with both XSLT engines. Forward Compatibility processing can be useful as part of a migration strategy.        

Another issue encountered when using a Custom XSLT file is the inability to use the xsl:import (and xsl:include) element. Any attempt to use this element will result in the error “XSLT compile error. Resolving of external URIs was prohibited.” But, by enabling the support for embedded script blocks in the XsltSettings class the xsl:import element can now be used.

 

Custom Third-Party Engine

A final Custom XSLT Transform implements a third-party engine: XmlPrime. This is an XSLT 2.0 engine with no custom extensions:

public class XmlPrimeTransform : ITransform2
{
    protected XmlPrime.XsltSettings xsltSettings;
    protected Xslt xslt;
 
    public XmlPrimeTransform()
    {
        this.xsltSettings = new XmlPrime.XsltSettings { ContextItemType = XdmType.Node, EnableScript = true };
    }
 
    public override void RegisterExtension(string namespaceUri, string assemblyName, string className)
    {
    }
 
    public override void Load(string xslt)
    {         
        this.xslt = Xslt.Compile(new StringReader(xslt), this.xsltSettings);
    }
 
    public override void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results)
    {
        var condextDocument = new XdmDocument(input, XmlSpace.Preserve);
        xslt.ApplyTemplates(condextDocument.CreateNavigator(), results);
    }
}
       

The full source code for the three custom transforms is given in Appendix C. The CustomTransform.xml file is configured as below:

<?xml version="1.0" encoding="utf-8" ?>
<CustomTransforms>
  <Transform
    DisplayName="Saxon 9 HE"
    TypeAssemblyQualifiedName="Microsoft.XLANGs.BaseTypes.SaxonHEXsltTransform, Microsoft.XLANGs.BaseTypes, Version=3.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
  <Transform
    DisplayName="Saxon 9 HE (Debug)"
    TypeAssemblyQualifiedName="CustomTransform.SaxonHEXsltTransform, CustomTransform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx, processorArchitecture=MSIL"/>
  <Transform
    DisplayName="XmlPrime"
    TypeAssemblyQualifiedName="CustomTransform.XmlPrimeTransform, CustomTransform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx, processorArchitecture=MSIL"/>
  <Transform
    DisplayName=".Net Framework XSLT 2.0"
    TypeAssemblyQualifiedName="CustomTransform.DNFXsltTransform, CustomTransform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx, processorArchitecture=MSIL"/>
</CustomTransforms>



5. Summary

The introduction of XSLT 3.0 is a welcomed new addition to the BizTalk Server mapper and gives the developer another tool to perform message transformation. Out of the box the existing BizTalk Server mapper functoids cannot be used though there are workarounds that can allow their use. It also brings the XSLT mapping capabilities of BizTalk Server into line with those of Logic Apps (with an Integration Account), but there are several differences and something to bear in mind if you’re looking to reuse or migrate any map to Azure Logic Apps. If, as a result of this new functionality, a migration from XSLT 1.0 to XSLT 3.0 is contemplated it needs careful consideration and planning. A number of different strategies in approaching this are discussed here.

6. References

BizTalk Server 2020:

What's New in BizTalk Server 2020

XSLT Transform Engine (Grid Property)

Custom XSLT transform implementation

 

Saxon 9 HE:

Saxon API for .NET

XSLT Elements

.NET extension functions: full interface

 

XSLT:

XSLT 1.0 Forwards-Compatible Processing

XSL Transformations (XSLT)Version 2.0

XSL Transformations (XSLT) Version 3.0

 

.NET:

XSLT Stylesheet Scripting Using <msxsl:script>

Xslt​Settings Class


7. Appendices

 

Appendix A

Functions can be added an external file and then imported using an xsl:import element in a scripting functoid using an Inline XSLT Call Template script types.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 
  <xsl:function name="userCSharp:StringConcat" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xsl:param name="param1" as="xs:string" />
    <xsl:param name="param2" as="xs:string" />
    <xsl:value-of select="concat($param1, $param2)" />
  </xsl:function>
 
  <xsl:function name="userCSharp:MathAdd" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xsl:param name="param1" as="xs:string" />
    <xsl:param name="param2" as="xs:string" />
    <xsl:value-of select="number($param1) + number($param2)" />
  </xsl:function>
 
</xsl:stylesheet>

Appendix B

Updated class to allow the function to be called using the External Assembly script type.

using System;
using System.Collections.Generic;
using Saxon.Api;
 
namespace CSharpExtensions
{
    /// <summary>
    /// Example extension function to compute a square root.
    /// </summary>
 
    public class Sqrt : ExtensionFunctionDefinition
    {
        public void sqrt(string param1)
        {
        }
 
        public override QName FunctionName
        {
            get
            {
                return new QName("http://schemas.microsoft.com/BizTalk/2003/ScriptNS0", "sqrt");
            }
        }
 
        public override int MinimumNumberOfArguments
        {
            get
            {
                return 1;
            }
        }
 
        public override int MaximumNumberOfArguments
        {
            get
            {
                return 1;
            }
        }
 
        public override XdmSequenceType[] ArgumentTypes
        {
            get
            {
                return new XdmSequenceType[]{
                    new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_STRING), '?')
                };
            }
        }
 
        public override XdmSequenceType ResultType(XdmSequenceType[] ArgumentTypes)
        {
            return new XdmSequenceType(XdmAtomicType.BuiltInAtomicType(QName.XS_DOUBLE), '?');
        }
 
        public override bool TrustResultType
        {
            get
            {
                return true;
            }
        }
 
 
        public override ExtensionFunctionCall MakeFunctionCall()
        {
            return new SqrtCall();
        }
    }
 
    internal class SqrtCall : ExtensionFunctionCall
    {
        public override IEnumerator<XdmItem> Call(IEnumerator<XdmItem>[] arguments, DynamicContext context)
        {
            Boolean exists = arguments[0].MoveNext();
            if (exists)
            {
                XdmAtomicValue arg = (XdmAtomicValue)arguments[0].Current;
                double val = (double)arg.Value;
                double sqrt = System.Math.Sqrt(val);
                XdmAtomicValue result = new XdmAtomicValue(sqrt);
                return (IEnumerator<XdmItem>)result.GetEnumerator();
            }
            else
            {
                return EmptyEnumerator<XdmItem>.INSTANCE;
            }
        }
    }

Appendix C

C# class library code with three custom transform engines:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Xsl;
using Microsoft.XLANGs.BaseTypes;
using Saxon.Api;
using XmlPrime;
 
namespace CustomTransform
{
    public class SaxonHEXsltTransform : ITransform2
    {
        protected bool legacyWhitespaceBehavior;
        protected Processor processor;
        protected XsltCompiler compiler;
        protected Xslt30Transformer transformer;
 
        public SaxonHEXsltTransform()
        {
            System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - Constructor");
 
            this.legacyWhitespaceBehavior = Microsoft.BizTalk.ScalableTransformation.BTSXslTransform.LegacyWhitespaceBehavior;
            this.processor = new Processor(true);
 
            this.processor.SetProperty(FeatureKeys.STRIP_WHITESPACE, this.legacyWhitespaceBehavior ? "all" : "none");
            this.compiler = processor.NewXsltCompiler();
            this.compiler.ErrorList = new List<StaticError>();
        }
 
        public override void RegisterExtension(string namespaceUri, string assemblyName, string className)
        {
            System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - RegisterExtension");
            System.Diagnostics.Trace.WriteLine("namespaceUri: " + namespaceUri + "assemblyName: " + assemblyName + "className: " + className);
 
            Assembly assembly = Assembly.Load(assemblyName);
            Object obj = assembly.CreateInstance(className);
 
            ExtensionFunction function = obj as ExtensionFunction;
            if (function != null)
            {
                this.processor.RegisterExtensionFunction(function);
            }
 
            ExtensionFunctionDefinition functionDefinition = obj as ExtensionFunctionDefinition;
            if (functionDefinition != null)
            {
                this.processor.RegisterExtensionFunction(functionDefinition);
            }
 
            if (function == null && functionDefinition == null)
            {
                System.Diagnostics.Trace.WriteLine(string.Format("Invalid extension class {0}, it should be of type {1} or {2}.", className, typeof(ExtensionFunction).Name, typeof(ExtensionFunctionDefinition).Name));
                throw new ArgumentException(string.Format("Invalid extension class {0}, it should be of type {1} or {2}.", className, typeof(ExtensionFunction).Name, typeof(ExtensionFunctionDefinition).Name));
            }
        }
 
        public override void Load(string xslt)
        {
            try
            {
                System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - Load");
                System.Diagnostics.Trace.WriteLine("XSLT: " + xslt);
 
                XsltExecutable executable = this.compiler.Compile(new StringReader(xslt));
                this.transformer = executable.Load30();
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine("");
                System.Diagnostics.Trace.WriteLine("XSLT Errors");
                System.Diagnostics.Trace.WriteLine("===========");
                foreach (StaticError se in this.compiler.ErrorList)
                {
                    System.Diagnostics.Trace.WriteLine("Message: " + se.Message);
                    System.Diagnostics.Trace.WriteLine("Column Number: " + se.ColumnNumber.ToString());
                    System.Diagnostics.Trace.WriteLine("Line Number: " + se.LineNumber.ToString());
                    System.Diagnostics.Trace.WriteLine("Source: " + se.Source);
                }
                System.Diagnostics.Trace.WriteLine("Exception: " + ex.ToString());
            }
        }
 
        public override void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results)
        {
            System.Diagnostics.Trace.WriteLine("SaxonHEXsltTransform - Transform");
 
            if (xsltArguments != null)
            {
                Dictionary<Saxon.Api.QName, XdmValue> parameters = new Dictionary<Saxon.Api.QName, XdmValue>();
                foreach (XmlQualifiedName name in xsltArguments.Keys)
                {
                    parameters[new Saxon.Api.QName(name)] = new XdmExternalObjectValue(xsltArguments[name]);
                }
 
                this.transformer.SetStylesheetParameters(parameters);
            }
 
            this.transformer.InputXmlResolver = new XmlUrlResolver();
            Serializer serializer = processor.NewSerializer();
            serializer.SetOutputStream(results);
            this.transformer.ApplyTemplates(input, serializer);
            serializer.Close();
        }
    }
 
    public class DNFXsltTransform : ITransform2
    {
        protected XsltArgumentList xsltArgumentList;
        protected XslCompiledTransform xslCompiledTransform;
        private static readonly System.Xml.Xsl.XsltSettings xsltSettings = new System.Xml.Xsl.XsltSettings(true, true);
 
        public DNFXsltTransform()
        {
            xsltArgumentList = new XsltArgumentList();
            xslCompiledTransform = new XslCompiledTransform();
        }
 
        public override void RegisterExtension(string namespaceUri, string assemblyName, string className)
        {
            Assembly assembly = Assembly.Load(assemblyName);
            object extension = assembly.CreateInstance(className);
            xsltArgumentList.AddExtensionObject(namespaceUri, extension);
        }
 
        public override void Load(string xslt)
        {
            XmlDocument xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(xslt);
 
            // Change the xslt version atttribute to enable Forward Compatibility
            xmlDocument.DocumentElement.RemoveAttribute("version");
            xmlDocument.DocumentElement.SetAttribute("version", "2.0");
 
            xslCompiledTransform.Load(xmlDocument, xsltSettings, new XmlUrlResolver());
        }
 
        public override void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results)
        {
            AddArguments(xsltArgumentList, xsltArguments);
            using (XmlWriter xmlWriter = XmlWriter.Create(results, xslCompiledTransform.OutputSettings))
            {
                xslCompiledTransform.Transform(new XmlTextReader(input), xsltArgumentList, xmlWriter, new XmlUrlResolver());
                xmlWriter.Flush();
            }
        }
 
        private void AddArguments(XsltArgumentList argumentList, IDictionary<XmlQualifiedName, object> xsltArguments)
        {
            if (xsltArguments != null)
            {
                foreach (XmlQualifiedName key in xsltArguments.Keys)
                {
                    if (argumentList.GetParam(key.Name, key.Namespace) != null)
                    {
                        argumentList.RemoveParam(key.Name, key.Namespace);
                    }
                    argumentList.AddParam(key.Name, key.Namespace, xsltArguments[key]);
                }
            }
        }
    }
 
    public class XmlPrimeTransform : ITransform2
    {
        protected XmlPrime.XsltSettings xsltSettings;
        protected Xslt xslt;
 
        public XmlPrimeTransform()
        {
            this.xsltSettings = new XmlPrime.XsltSettings { ContextItemType = XdmType.Node, EnableScript = true };
        }
 
        public override void RegisterExtension(string namespaceUri, string assemblyName, string className)
        {
        }
 
        public override void Load(string xslt)
        {         
            this.xslt = Xslt.Compile(new StringReader(xslt), this.xsltSettings);
        }
 
        public override void Transform(Stream input, IDictionary<XmlQualifiedName, object> xsltArguments, Stream results)
        {
            var condextDocument = new XdmDocument(input, XmlSpace.Preserve);
            xslt.ApplyTemplates(condextDocument.CreateNavigator(), results);
        }
    }
 
}