1. Introduction

In a previous series (see: Migrating BizTalk Maps to Azure Logic Apps: Shortcomings & Solutions (part 1)  & (part 2)) a number of limitations were highlighted when trying to migrate BizTalk Server maps to Azure Logic Apps. It was also mentioned that in the November 2017 Logic Apps Live Webcast future support for extension objects was announced and that there was a restriction with the Logic App Transform XML action not being able to use an XSLT map that produces a non-XML output. Both these limitations have now been resolved. One of the consequences of this is that it now provides a mechanism whereby a custom class method can now be called from a Logic App rather than, say, calling an Azure Function albeit in an indirect way.

2. Solution Overview

The solution will make use of an <msxsl:script> block with an XSLT map to load and execute class method contained in the assembly uploaded to the Integration Account Assemblies blade. The function will be called by utilizing a Transform XML action in the Logic App and the method result will be returned as the Transform XML body. The XSLT map will need to o the following:

  • Configure an XSLT to accept a method name, a parameter list for the method, and to return non-XML output;
  • Dynamically create the method parameter list casting each parameter from the supplied parameters, and then invoke the method;
  • Ignore the source XML body and only execute the specified method;
  • Return the method response as non-XML XSLT output;
  • Configure a Transform XML action to call the XSLT map and accept text (non-XML) response.

3. Integration Account Custom Assembly

Create a new C# class library named: ExtAssembly. Replace the default Class1.cs contents (and rename the filename) with the following:

using System;
namespace ExtAssembly
{
 public static class CalcFunctions
 {
 public static Int64 Add(Int64 a, Int64 b)
 {
 return a + b;
 }
 public static Int64 Subtract(Int64 a, Int64 b)
 {
 return a - b;
 }
 public static Int64 Multiply(Int64 a, Int64 b)
 {
 return a * b;
 }
 public static Double Divide(Int64 a, Int64 b)
 {
 return a / b;
 }
 }
}

Set the default namespace to ExtAssembly and sign the assembly with a Strong Name. Build the assembly and upload it to an Azure Logic App Integration Account to the Assemblies blade, as show below:

 

4. Integration Account XSLT Map

Create a new XSLT file named: CallExtAssembly.xslt. Copy the following contents into the new 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" exclude-result-prefixes="msxsl userCSharp" version="1.0" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
 <xsl:output omit-xml-declaration="yes" media-type="application/text" method="text" version="1.0" />
 <xsl:param name="MethodName" />
 <xsl:param name="Parameters" />
 <xsl:template match="/">
 <xsl:value-of select ="userCSharp:Invoke($MethodName, substring-before(substring-after($Parameters, '('), ')'))" />
 </xsl:template>
 <msxsl:script language="C#" implements-prefix="userCSharp"
 <msxsl:assembly name="ExtAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx" />
 <msxsl:using namespace="System.Reflection" />
 <msxsl:using namespace="System.Text.RegularExpressions" />
 <msxsl:using namespace="ExtAssembly" />
 <![CDATA[
 public object Invoke(string methodName, string methodParameters)
 {
 MatchCollection matches = new Regex("((?<=\")[^\"]*(?=\"(,|$)+)|(?<=,|^)[^,\"]*(?=,|$))").Matches(methodParameters); 
 ParameterInfo[] pars = typeof(CalcFunctions).GetMethod(methodName).GetParameters();
 object[] methodPars = new object[pars.Length]; 
 for (int i = 0; i < pars.Length; i++)
 {
 methodPars[i] = Convert.ChangeType(matches[i].Value, pars[i].ParameterType);
 }
 return typeof(CalcFunctions).GetMethod(methodName).Invoke(null, methodPars);
 }
 ]]>
</msxsl:script>
</xsl:stylesheet>
  

Upload the XSLT map to an Azure Logic App Integration Account to the Maps blade.

 

5. Testing in a Logic App

Create a new Logic App (When a HTTP Request is received). Add an Initialize variable action, a do until loop, a Transform XML action, a Set variable action, and a HTTP Response action as shown below:

 

Set the Initialize variable name, type and value as count, Integer, and 1. Configure the Until loop condition to count is greater than 10 as shown in the image. Configure the Transform XML action as follows:

Note that the Content must contain a value even though it is ignored in the map. Select the CallExtAssembly map from the dropdown list. Remember to set the Transform options (in advanced options) to Generate text output. Set the MethodName to the CalcFunctions class method to call, in this case, Add, and set the Parameters value to match the Add method’s require parameters: the count variable, and the number 1, within brackets as shown. In the Set variable action cast the output of the Transform XML action body to an integer as shown below:

 

Finally, in the HTTP Response action set the Body value to the count variable. Run the Logic App. The following run history should be seen with a count value output of 11:

 

   

6. Some Further Considerations

There are a few things to considered when using this mechanism of calling a ‘function’ from Logic Apps as opposed to calling an Azure function: 

  • The method return value is returned in the Transform XML action body as text. It must be cast to the required data type if used in subsequent actions;
  • This example makes use of static methods. If an instance method is required, then an instance of the class will need to be instantiated in the XSLT script block; 
  • The XSLT script block does not take into account overloaded methods or optional and output parameters. If these are required, they will need to be coded for;
  • The XSLT script block does not support parameter arrays and any attempt to use them will result in an exception similar to: System.Xml.Xsl.XslTransformException: Extension function parameters or return values which have Clr type 'String[]' are not supported.;
  • No consideration has been given to any performance related differences between calling an Integration Account Assembly function and an Azure Function;
  • If the method being called has no parameters, simply pass in () - as the Parameter value cannot be empty;
  • See Appendix A for an alternate XSLT map that accepts an assembly name and class name as runtime parameters (assumes the filename and default namespace match). 

 

7. Summary

The purpose of this article is to provide a mechanism for calling a class method in an assembly uploaded to a Logic App Integration Account as an alternative to calling an Azure Function. One consequence of this is that the function is run in the context of a Logic Apps SLA, which may be beneficial if the alternative is an Azure Function running under a Consumption Plan (which provides no SLA).  It also serves to demonstrate how to call an external function from a BizTalk XSLT map migrated to Azure Logic Apps that had previously used the support for extension objects common within BizTalk mapping, and how to return non-XML output from an XSLT transform in Logic Apps.

 

8. References

 

9. Appendix A

The XSLT below loads the assembly and retrieves the class and method details without using the <msxsl:assembly> element. The assembly qualified name and the class name are passed as XSLT parameters: 


<?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" exclude-result-prefixes="msxsl userCSharp" version="1.0" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
  <xsl:output omit-xml-declaration="yes" media-type ="application/text" method="text" version="1.0" />
  <xsl:param name="AssemblyName" />
  <xsl:param name="ClassName" />
  <xsl:param name="MethodName" />
  <xsl:param name="Parameters" />
  <xsl:template match="/">
    <xsl:value-of select ="userCSharp:Invoke($AssemblyName, $ClassName, $MethodName, substring-before(substring-after($Parameters, '('), ')'))" />
  </xsl:template>
  <msxsl:script language="C#" implements-prefix="userCSharp">   
    <msxsl:using namespace="System" />
    <msxsl:using namespace="System.Reflection" />
    <msxsl:using namespace="System.Text.RegularExpressions" />
    <![CDATA[
        public object Invoke(string assemblyName, string className, string methodName, string methodParameters)
        {
              // Load assembly and get class
              var an = new AssemblyName(assemblyName);
              var assembly = Assembly.Load(an);
              Type myType = assembly.GetType(assembly.GetName().Name + "." + className);
             
              // Parse the parameter list
              MatchCollection matches = new Regex("((?<=\")[^\"]*(?=\"(,|$)+)|(?<=,|^)[^,\"]*(?=,|$))").Matches(methodParameters);           
              ParameterInfo[] pars = myType.GetMethod(methodName).GetParameters();
             
              // Create an array of parameters cast to their correct type
              object[] methodPars = new object[pars.Length];           
              for (int i = 0; i < pars.Length; i++)
              {
                  methodPars[i] = Convert.ChangeType(matches[i].Value, pars[i].ParameterType);
              }
             
              // Execute method and return the result
              return myType.GetMethod(methodName).Invoke(null, methodPars);
        }
  ]]>
</msxsl:script
</xsl:stylesheet>

The Transform XML action now looks like the following: