Share via


BizTalk Maps: Migrating to Azure Logic Apps: Shortcomings & Solutions (part 1)

1. Introduction

Within Logic Apps there exits an Integration Account, part of the Logic Apps Enterprise Integration Pack (EIP), that can store maps generated by the BizTalk mapper (viz. XSLT). These maps can then be used in a Logic App XML Transform action to perform message translation. The focus of this article is to highlight possible issues that may arise when a BizTalk map is migrated to Logic Apps and how these issues can be overcome. This article also considers the migration of a BizTalk map to Logic Apps when either the XML Transform action cannot be used or when no Integration Account is available.

It should be remembered that the BizTalk mapper designer tool does not simply provide translation, it allows the implementation of specific integration patterns (e.g. message aggregator and message splitter) which also need to be taken into consideration.

Essentially the migration of a BizTalk map will fit into one of three categories:

  1. The map will migrate without any modification. The maps included in this category will be ones that have a simple single source schema to a single destination schema mapping, use no database functoids or custom functoids, and make no calls to external assemblies using the Scripting functoid;

  2. The map will require modification. These maps will either require a change or will need additional Logic App steps to construct the correct input (or extract the correct output) for the map. An example of this would be constructing the AggSchema envelope message for a multi-input map;

  3. The map cannot be migrated and a solution other than using the Logic App's XML Transform action is required. This would include those maps that use database functoids, custom functoids, and make calls to external assemblies.

Finally, there is also a brief consideration of mapping done within BizTalk which does not make use of the BizTalk mapper  (i.e. in a pipeline or orchestration) , for example, by serializing/de-serializing to and from .NET classes or by using a third-party XSLT translator.

This article assumes the reader has a basic knowledge of Azure Logic Apps, Logic Apps Enterprise Integration Pack, Logic Apps XML Transform action, BizTalk Server mapper designer tool, C#, XML, XSLT, and familiarity with Microsoft Visual Studio.

2. Overview of Migrating BizTalk Maps to Logic Apps’s Transform XML Action

The Logic Apps EIP can be used to store the XSLT of a map generated by the BizTalk mapper. The process of generating the XSLT of a BizTalk map is straight forward: in Visual Studio right-click on the map in the Solution Explorer window, select validate map, and the XSLT file location will appear in the Output window. The file can then be uploaded to the Integration Account. This is basic BizTalk mapper knowledge so there is no need to dive in deeper and anyone requiring further information can find numerous examples online.

A BizTalk map that does not make use for database functoids, custom functoids, and make no calls to external assemblies using the Scripting functoid will migrate to Logic Apps and the XML Transform action without any issues. At present there in one exception to this. The Logic App XML Transform action output must be XML, if it is not the map will fail. So, if a map with an XSLT template in a scripting functoid is used to generate non-XML output it will fail with the exception: “InternalServerError. The 'Xslt' action failed with error code 'InternalServerError'.” As of 6th June 2017 Microsoft have acknowledged this limitation and stated that they are working to address this.

If the BizTalk map cannot be migrated ‘as is’ it will be because it fits into either category 2 or 3 explained in the introduction. In the following sections each migration issue is discussed and a solution provided. In some cases alternative solutions are given.

3. How to Pass Multiple Source Messages to a Transform XML Action (Method 1)

Problem*: If a BizTalk map has multiple source messages.*

Solution*: Manually build the envelope message from the multiple source messages.*

In BizTalk it is possible to create a map that has multiple source (and target) schemas. But this is really an artificial construct; the map only has a single source/target schema. This is because behind the scenes BizTalk creates an ‘envelope’ schema that wraps the multiple source schemas. An example of such a schema can be seen if you test a multi-input message map in BizTalk – ensuring the TestMap Input property is set to Generate Instance – in the Output window will appear a link to the test file used in testing the map:

TestMap used the following file: <file:///C:\Users\..\inputfile.xml> as input to the map

If the file is opened it should be similar to the one below:

<ns0:Root xmlns:ns0="http://schemas.microsoft.com/BizTalk/2003/aggschema">
  <InputMessagePart_0>
    <ns1:Person xmlns:ns1="http://AzureBizTalkMapsDemo/Person">
      <Title>Title_0</Title>
      <Forename>Forename_0</Forename>
      <Surname>Surname_0</Surname>
    </ns1:Person>
  </InputMessagePart_0>
  <InputMessagePart_1>
    <ns2:Address xmlns:ns2="http://AzureBizTalkMapsDemo/Address">
      <Street>Street_0</Street>
      <City>City_0</City>
      <State>State_0</State>
      <ZipCode>ZipCode_0</ZipCode>
    </ns2:Address>
  </InputMessagePart_1>
</ns0:Root>

This first solution to passing multiple source messages simply involves building an ‘envelope’ message similar to that above for the input messages.

In the Logic App add a Transform XML action and add the ‘envelope’ elements (highlighted in the preceding example) directly into the Content around the source messages. For example:

It may be easier to make these edits in the Logic App Code View rather than the Designer. The code will appear similar to this below:

 

"Transform_XML:_Contact": {
    "inputs": {
        "content": "<ns0:Root xmlns:ns0='http://schemas.microsoft.com/BizTalk/2003/aggschema'><InputMessagePart_0> @{body('Get_file_content_using_path:_Person')}</InputMessagePart_0><InputMessagePart_1>@{body('Get_file_content_using_path:_Address')}</InputMessagePart_1></ns0:Root>",
        "integrationAccount": {
            "map": {
                "name": "MultiSourceMsgsToContactMsg"
             }
        }
    },

 

The Transform XML action will concatenate both the ‘envelope’ elements and source messages and pass the content as a single source message to the XSLT processor.

 

Pros*: Simple to implement. The BizTalk XSLT map requires no changes.*

Cons*: Looks untidy and difficult to edit in the Logic App design view.*

4. How to Pass Multiple Source Messages to a Transform XML Action (Method 2)

Problem*: If a BizTalk map has multiple source messages.*

Solution*: Pass each source message as an XSLT parameter. Convert each parameter value to an XPathModeIterator object and store it in an XSLT variable with the same name as the corresponding ‘envelope’ input message node. Update each XPath query to use the appropriate XSLT variable.*

This second method passes the input messages as stylesheet parameters. The benefit of this approach is there is no need construct the ‘envelope’ source message. However, this method does require changes to the XSLT stylesheet. The changes are as follows:

  1. Add an xsl:param element for each input message;

  2. Add a new xsl:variable for each input message the name of which should be that of the wrapper node generate by the BizTalk Mapper (in the form InputMessagePart_n);

  3. The Transform XML action will pass the input messages as strings. These will need converting to an XPathNodeIterator before they can be used. Copy the msxml:script in the example below into the stylesheet;

  4. Call a C# script function ConvertXMLStringToXPathNodeIterator in the variable select attribute to convert the input messages;

  5. Perform a search and replace with the XSLT to add a ‘$’ character to the beginning of the xsl:value-of select statements, e.g. replace InputMessagePart_0 with **$**InputMessagePart_0.

An example of the changes is given in the stylesheet below:

<?xml version="1.0" encoding="UTF-16"?>
<xsl:stylesheet version="1.0" 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" xmlns:s0="http://AzureBizTalkMapsDemo/Person" xmlns:ns0="http://AzureBizTalkMapsDemo/Contact" xmlns:s1="http://AzureBizTalkMapsDemo/Address" xmlns:s2=http://schemas.microsoft.com/BizTalk/2003/aggschema xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp" exclude-result-prefixes="msxsl var s1 s0 s2 userCSharp">
  <xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
  <xsl:param name ="Person" />
  <xsl:param name ="Address" />
   
  <xsl:variable name ='InputMessagePart_0' select='userCSharp:ConvertXMLStringToXPathNodeIterator($Person)' />
  <xsl:variable name ='InputMessagePart_1' select ='userCSharp:ConvertXMLStringToXPathNodeIterator($Address)' />
   
  <xsl:template match="/">
    <xsl:apply-templates select="/s2:Root" />
  </xsl:template>
  <xsl:template match="/s2:Root">
    <ns0:Contact>
      <Title>
        <xsl:value-of select="$InputMessagePart_0/s0:Person/Title/text()" />
      </Title>
      <Forename>
        <xsl:value-of select="$InputMessagePart_0/s0:Person/Forename/text()" />
      </Forename>
      <Surname>
        <xsl:value-of select="$InputMessagePart_0/s0:Person/Surname/text()" />
      </Surname>
      <Street>
        <xsl:value-of select="$InputMessagePart_1/s1:Address/Street/text()" />
      </Street>
      <City>
        <xsl:value-of select="$InputMessagePart_1/s1:Address/City/text()" />
      </City>
      <State>
        <xsl:value-of select="$InputMessagePart_1/s1:Address/State/text()" />
      </State>
      <ZipCode>
        <xsl:value-of select="$InputMessagePart_1/s1:Address/ZipCode/text()" />
      </ZipCode>
    </ns0:Contact>
  </xsl:template>
<msxsl:script language="C#" implements-prefix="userCSharp">
  <msxsl:using namespace="System.IO" />
  <msxsl:using namespace="System.Xml.XPath" />
  <![CDATA[
  public XPathNodeIterator ConvertXMLStringToXPathNodeIterator (string XML)
  {
      using (StringReader sr = new StringReader(XML))
      {
          XPathDocument xpd = new XPathDocument(sr);
          XPathNavigator xpn = xpd.CreateNavigator();
          return xpn.Select("/");
      }
  }
  ]]>
</msxsl:script>
</xsl:stylesheet>

 

In the Logic App Transform action, when the multi-input map is selected the stylesheet parameters will be displayed. These can be populated with the input XML messages, e.g.:

Note that a Content value must be provided. For this simply use an empty root node element:

<ns0:Root xmlns:ns0='http://schemas.microsoft.com/BizTalk/2003/aggschema' />

 

Pros*: Looks tidy and easy to read. No need to construct the source message.*

Cons*: The BizTalk XSLT map requires changes.*

5. How to Return Multiple Target Messages from a Transform XML Action (Method 1)

Problem*: If a BizTalk map has multiple target messages.*

Solution*: The XSLT Transform will return an envelope message. Use @XPath in Compose shapes to extract the target messages.*

BizTalk maps allow the construction of multiple target messages. The way this is achieved is similar to how multiple source messages are processed. The BizTalk XSLT map output constructs an envelope message. In the orchestration where the map is used the target message are extracted from the OutputMessagePart_n nodes. For example, below is the XSLT from a BizTalk map that creates two target messages:

<?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 s0" version="1.0" xmlns:ns2="http://AzureBizTalkMapsDemo/Address" xmlns:s0="http://AzureBizTalkMapsDemo/Contact" xmlns:ns1="http://AzureBizTalkMapsDemo/Person" xmlns:ns0="http://schemas.microsoft.com/BizTalk/2003/aggschema">
  <xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
  <xsl:template match="/">
    <xsl:apply-templates select="/s0:Contact" />
  </xsl:template>
  <xsl:template match="/s0:Contact">
    <ns0:Root>
      <OutputMessagePart_0>
        <ns1:Person>
          <Title>
            <xsl:value-of select="Title/text()" />
          </Title>
          <Forename>
            <xsl:value-of select="Forename/text()" />
          </Forename>
          <Surname>
            <xsl:value-of select="Surname/text()" />
          </Surname>
        </ns1:Person>
      </OutputMessagePart_0>
      <OutputMessagePart_1>
        <ns2:Address>
          <Street>
            <xsl:value-of select="Street/text()" />
          </Street>
          <City>
            <xsl:value-of select="City/text()" />
          </City>
          <State>
            <xsl:value-of select="State/text()" />
          </State>
          <ZipCode>
            <xsl:value-of select="ZipCode/text()" />
          </ZipCode>
        </ns2:Address>
      </OutputMessagePart_1>
    </ns0:Root>
  </xsl:template>
</xsl:stylesheet>

       

The solution in the Logic App is straightforward and the XSLT does not need editing. The require nodes simply need extracting from the Transform XML action output. The steps are:

  1. Add a Compose action to extract the message;

  2. Add the following to the inputs parameter, with the highlighted being replace with the name of the Transform XML action name:

    @xpath(xml(body('Transform_XML:_ContactMsgToMultiTargetMsgs')), '/*[local-name()="Root" and namespace-uri()="http://schemas.microsoft.com/BizTalk/2003/aggschema"]/*[local-name()="OutputMessagePart_0" and namespace-uri()=""]/*[1]')

  3. The @xpath function returns an array. In order to reference the target message an index must be specified. For example, @xml(outputs('Compose:_PersonMsg')[0]) (note that it is a zero-indexed array).

The Logic App should look similar to the following:

*** ***

Pros*: The BizTalk XSLT map does not require changes.*

Cons*: The target messages must be extracted using a Compose shapes and XPath queries.*

6. How to Return Multiple Target Messages from a Transform XML Action (Method 2)

Problem*: If a BizTalk map has multiple target messages.*

Solution*: Modify the BizTalk XSLT map to return a JSON object that contains each of the target messages.*

In part 2 of this article (section 5) is described how to generate an output from an XSLT map that is not XML. So, as an alternative to the previous approach of returning multiple target messages, it is possible to return a JSON object containing the target XML messages that are base 64 encoded. To do this changes will need to be made to the XSLT map:

  1. modify the xsl:output value;

  2. wrap each target message in an xsl:variable;

  3. create the output JSON object;

  4. base64 encode each target message.

The highlighted lines below in the sample XSLT show these required changes:

<?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 s0" version="1.0" xmlns:ns2="http://AzureBizTalkMapsDemo/Address" xmlns:s0="http://AzureBizTalkMapsDemo/Contact" xmlns:ns1="http://AzureBizTalkMapsDemo/Person" xmlns:ns0="http://schemas.microsoft.com/BizTalk/2003/aggschema" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
  <xsl:output media-type ="application/json" method="text" />
  <xsl:template match="/">
    <xsl:apply-templates select="/s0:Contact" />
  </xsl:template>
  <xsl:template match="/s0:Contact">
    <xsl:variable name ="Person" >
        <ns1:Person>
          <Title>
            <xsl:value-of select="Title/text()" />
          </Title>
          <Forename>
            <xsl:value-of select="Forename/text()" />
          </Forename>
          <Surname>
            <xsl:value-of select="Surname/text()" />
          </Surname>
        </ns1:Person>
    </xsl:variable>
    <xsl:variable name ="Address" >
        <ns2:Address>
          <Street>
            <xsl:value-of select="Street/text()" />
          </Street>
          <City>
            <xsl:value-of select="City/text()" />
          </City>
          <State>
            <xsl:value-of select="State/text()" />
          </State>
          <ZipCode>
            <xsl:value-of select="ZipCode/text()" />
          </ZipCode>
        </ns2:Address>
    </xsl:variable>
     
  <xsl:text>{"Person": "</xsl:text>
  <xsl:copy-of select ="userCSharp:ToBase64(msxsl:node-set($Person))" />
  <xsl:text>", "Address": "</xsl:text>
  <xsl:copy-of select ="userCSharp:ToBase64(msxsl:node-set($Address))" />
  <xsl:text>"}</xsl:text>
     
</xsl:template>
  <msxsl:script language="C#" implements-prefix="userCSharp">
    <msxsl:using namespace="System.Xml.XPath" />
    <![CDATA[
  public string ToBase64(XPathNodeIterator nodes)
  {
      if (nodes.MoveNext())
      {
        var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(nodes.Current.InnerXml);
        return System.Convert.ToBase64String(plainTextBytes);
      }
      else
      {
        return String.Empty;
      }
  }
  ]]>
  </msxsl:script>
</xsl:stylesheet>

       

At the moment, because of its limitations, the Transform XML action cannot be used to perform this transformation. Instead an Azure function is used (more on this in part 2 of the article). However, the Logic App will look similar to that given below. Notice that the XSLT Map to be used is first looked up using an Integration Account Artifact Lookup action. The output – the contentLink.uri – is then passed in the header to the Azure Function:

       

The response is parsed using a Parse JSON action.

Example JSON output:

{
"Person": "PG5zMTpQZXJzb24geG1sbnM6bnMxPSJodHRwOi8vQXp1cmVCaXpUYWxrTWFwc0RlbW8vUGVyc29uIiB4bWxuczpuczI9Imh0dHA6Ly9BenVyZUJpelRhbGtNYXBzRGVtby9BZGRyZXNzIiB4bWxuczpuczA9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vQml6VGFsay8yMDAzL2FnZ3NjaGVtYSIgeG1sbnM6dXNlckNTaGFycD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9CaXpUYWxrLzIwMDMvdXNlckNTaGFycCI+DQogIDxUaXRsZT5Nci48L1RpdGxlPg0KICA8Rm9yZW5hbWU+RGF2aWQ8L0ZvcmVuYW1lPg0KICA8U3VybmFtZT4iezorOn17PC9TdXJuYW1lPg0KPC9uczE6UGVyc29uPg==",
"Address": "PG5zMjpBZGRyZXNzIHhtbG5zOm5zMj0iaHR0cDovL0F6dXJlQml6VGFsa01hcHNEZW1vL0FkZHJlc3MiIHhtbG5zOm5zMT0iaHR0cDovL0F6dXJlQml6VGFsa01hcHNEZW1vL1BlcnNvbiIgeG1sbnM6bnMwPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0JpelRhbGsvMjAwMy9hZ2dzY2hlbWEiIHhtbG5zOnVzZXJDU2hhcnA9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vQml6VGFsay8yMDAzL3VzZXJDU2hhcnAiPg0KICA8U3RyZWV0Pk1haW4gU3RyZXQ8L1N0cmVldD4NCiAgPENpdHk+TWlhbWk8L0NpdHk+DQogIDxTdGF0ZT5GTDwvU3RhdGU+DQogIDxaaXBDb2RlPjMzMTIwPC9aaXBDb2RlPg0KPC9uczI6QWRkcmVzcz4="
}

 

Pros: Looks tidy and easy to read. No need to extract the target messages.

Cons*: The BizTalk XSLT map requires changes. At present will not work with the Logic App XML Transform action.*

7. How to Use an XSLT Map with XSL:Import and XSL:Include (Method 1)

Problem*: If a BizTalk map uses a Scripting functoid to call a common external assembly function(s).*

Solution*: Migrate the external assembly function(s) to a new common XSLT file. Modify the XSLT to import the common XSLT file and call these script function(s) within the XSLT.*

A common use of using external assemblies in a BizTalk map is to encapsulate code that is shared across multiple maps (e.g. a custom date format function), rather than repeatedly copying the same function code into each Scripting functoid. An alternative solution to using an external assembly, but without having to replicate the code in each Scripting functoid, is to use a separate XSLT file and import it into the BizTalk XSLT map. Copy and paste the external assembly code into an msxsl:script block in a stylesheet like this (functions.xslt):

<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 ScriptNS0" version="1.0" xmlns:ScriptNS0="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
  <msxsl:script language="C#" implements-prefix="ScriptNS0"><![CDATA[
public string FirstLetterToUpper(string str)
{
    if (str == null)
        return null;
  
    if (str.Length > 1)
        return char.ToUpper(str[0]) + str.Substring(1);
  
    return str.ToUpper();
}
]]></msxsl:script>
</xsl:stylesheet>

       

The BizTalk XSLT map can then be modified to import this functions.xslt stylesheet and the extension object ScriptNS0 namespace modified to match that of the script namespace (userCSharp) as show below:

<?xml version="1.0"?>
<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 s2 s1 s0 userCSharp" version="1.0" xmlns:s2="http://AzureBizTalkMapsDemo.Customer" xmlns:s0="http://AzureBizTalkMapsDemo/Person" xmlns:ns0="http://AzureBizTalkMapsDemo/Contact" xmlns:s1="http://AzureBizTalkMapsDemo/Address" xmlns:userCSharp="http://schemas.microsoft.com/BizTalk/2003/userCSharp"
xmlns:ScriptNS0="http://schemas.microsoft.com/BizTalk/2003/userCSharp">
  <xsl:import href=" https://logicappsamples.blob.core.windows.net/publicfiles/functions.xslt " />
  
  <xsl:output omit-xml-declaration="yes" method="xml" version="1.0" />
  <xsl:template match="/">
    <xsl:apply-templates select="/s2:Customer" />
  </xsl:template>
  <xsl:template match="/s2:Customer">
    <ns0:Contact>
      <Title>
        <xsl:value-of select="s0:Person/Title/text()" />
      </Title>
      <Forename>
        <xsl:value-of select="ScriptNS0:FirstLetterToUpper(s0:Person/Forename/text())" />
      </Forename>
      <Surname>
        <xsl:value-of select="ScriptNS0:FirstLetterToUpper(s0:Person/Surname/text())" />
      </Surname>
      <Street>
        <xsl:value-of select="s1:Address/Street/text()" />
      </Street>
      <City>
        <xsl:value-of select="s1:Address/City/text()" />
      </City>
      <State>
        <xsl:value-of select="s1:Address/State/text()" />
      </State>
      <ZipCode>
        <xsl:value-of select="s1:Address/ZipCode/text()" />
      </ZipCode>
    </ns0:Contact>
  </xsl:template>
</xsl:stylesheet>

       

This solution allows the Logic Apps Transform XML action to be used, but first another issue needs to be resolved. Unfortunately, unlike integration account schemas which can use xsd:import  and simply specify the file name of the XSD to import, as a relative path is assumed, the same is not true for xsl:import. If an XSLT map uses xsl:import to import an uploaded map in a Transform XML action in Logic Apps the following error is generated:

"BadRequest. The 'Xslt' action failed due to incompatible map '<map name>'. Error details: 'An error occurred while processing map. 'XSLT compile error.''.

If, on the other hand, the map CONTENT LINK is specified (e.g. https://prod‑18.northcentralus.logic.azure.com/integrationAccounts/{GUID}/maps/{MapName}/contents/Value?api‑version=2015‑08-01‑preview&se=2017‑06‑06T17%3A27%3A50.1054230Z&sp=%2Fmaps%2F{MAPNAME}%2Fread&sv=1.0&sig={Signature}) the value is too long, so when the Logic App is run the Transform XML action returns the following error:

"BadRequest. The 'Xslt' action failed due to incompatible map '<map name>'. Error details: 'Error(s) occured while compiling the Xslt. Error Details: https:// ....logic.azure.com/integrationAccounts/.../maps/import/ contents/Value?api-version=2015-08-01-preview&se=... : error CS1560: Invalid filename specified for preprocessor directive. Filename is too long or not a valid filename.'."

The issue here is that the URI is longer than MAX_PATH or 260 characters (see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396).

However, there are a few solutions to overcome this limitation:

Host The XSLT File on a web site that is publicly accessible

If there are no security or intellectual property issues with the content of the XSLT map then it could be publicly hosted on a web site so long as the URL is no longer than MAX_PATH. For an example of this see: https://az818438.vo.msecnd.net/functoids/functoidsscript.xslt. Static content can be stored in an Azure Blob storage which can be accessed directly, e.g.: https://logicappsamples.blob.core.windows.net/publicfiles/functions.xslt

See /en-us/azure/architecture/patterns/static-content-hosting

Write a URL shortening service in Azure

The second method is to create a URL shortening service that redirects to the CONTENT LINK URI. There are a number of examples on how to do this in Azure (e.g. see http://onthecloud.azurewebsites.net/personal-url-shortening-service-with-azure-storage).

Use Azure API Management to publish the URI

Use Azure API Management to publish a shorten URI to the XSLT file (see /en-us/azure/api-management).

Note: Testing an XSLT map in Visual Studio that include xsl:import or xsl:include elements, by right-clicking on the BizTalk map and selecting Test Map, will fail with the exception: XSLT compile error. Resolving of external URIs was prohibited. The XSLT must be tested by opening the XSLT file and selecting XML | Start XSLT Debugging from the Visual Studio menu bar.

Pros*: The Logic App XSLT Transform action can be used. Requires minimal changes to the BizTalk XSLT map.*

Cons*: Will not work for functions that have parameter arrays or return arrays.*

8. How to Call an Azure Function from a Logic App XML Transform Action XSLT Map

Problem*: If a BizTalk map uses a Scripting functoid to call an external assembly function.*

Solution*: Migrate the external assembly function to a new Azure Function. Modify the BizTalk XSLT map to call the new Azure Function.*

The following sample code demonstrates how to call an Azure Function from an XSLT map that is used within a Logic App Transform XML action. A JSON object containing a list of Azure Functions is passed as an xsl:param to the XSLT map. The list is deserialized into a IDictionary object. To call the desired Azure Function in the XSLT map a C# script method CallAzureFunction is invoked with the Azure Function name and any parameters that the function expects. The CallAzureFunction method looks up the Azure Function URL in the IDictionary and creates a WebRequest object to call the Azure Function. The response from the Azure Function is return to the calling XSLT script method.

The implementation of this is as follows:

Create an Azure Function    

  1. Create an Azure Function using either a HttpTrigger or HttpTriggerWithParameters template:

    public static  async Task<HttpResponseMessage> Run(HttpRequestMessage req, string customerNum, TraceWriter log)
    {    
        log.Info("C# HTTP trigger function processed a request.");
         
        string name;
        switch (customerNum)
        {
            case "1000":
                name = "David";
                break;
            case "2000":
                name = "John";
                break;
            default:
                name = "Unknown";
                break;
        }
        return req.CreateResponse(HttpStatusCode.OK, name);
    }
    

     

  2. Configure the Route template (on the Integrate tab) for the parameter: GetCustomerName/customerNum/{customerNum}

  3. Make a note of the URL: https://tcdemos.azurewebsites.net/api/GetCustomerName/customerNum/{num}?code= ... ==

Edit the XSLT map that will call the Azure Function

  1. Add an xsl:param element named AzureFunctions to the top of the stylesheet:

    <xsl:param name="AzureFunctions"/>

  2. Add an xsl:variable element below the xsl:param element added in step 1:

    <xsl:variable name="deserializeFunctionList" select="userCSharp:DeserializeAzureFunctions($AzureFunctions)"></xsl:variable>

  3. Add the following script to the msxsl:script element:

    <msxsl:script language="C#" implements-prefix="userCSharp">
        <msxsl:assembly name="System.Web.Extensions"/>
        <msxsl:using namespace="System.IO" />
        <msxsl:using namespace="System.Net" />
        <msxsl:using namespace="System.Web.Script.Serialization" />
        <![CDATA[
    System.Collections.Generic.IDictionary<string, object> _azureFunctions;
    public void DeserializeAzureFunctions(string azureFunctions)
    {
    //Deserialise JSON Azure function list
    var jss = new JavaScriptSerializer();
    _azureFunctions = (System.Collections.Generic.IDictionary<string, object>)jss.DeserializeObject(azureFunctions);
    }
    public string CallAzureFunction(string functionName, string functionParameters)
    {
        //Split function parameters
        string[] parameters = functionParameters.Split(',');
         
        //Get function URL
        string url = _azureFunctions[functionName].ToString();
        foreach (string p in parameters)
        {
            //Replace the {} place holders with parameter values
            url = url.Replace(url.Substring(url.IndexOf('{'), url.IndexOf('}') - url.IndexOf('{') + 1), p);
        }
      
        //Call the Azure function
        using (WebResponse wr = WebRequest.Create(url).GetResponse())
        {
            using (StreamReader sr = new StreamReader(wr.GetResponseStream()))
            {
                return sr.ReadToEnd();
            }
        }
    }
    public string CallAzureFunction(string functionName, string parameter1, string parameter2)
    {
        return CallAzureFunction(functionName, parameter1 + "," + parameter2);
    }
    public string CallAzureFunction(string functionName, string parameter1, string parameter2, string parameter3)
    {
        return CallAzureFunction(functionName, parameter1 + "," + parameter2 + "," + parameter3);
    }
      
        ]]>
    </msxsl:script>
    

                                

    For further information on msxml:script see: https://msdn.microsoft.com/en-us/library/533texsx(v=vs.110).aspx

  4. The Azure Function can now be invoked by using code similar to the following:

    <ns0:CustomerName>

      <xsl:value-of select="userCSharp:CallAzureFunction('GetCustomerName',Invoice/@CustomerNum)" />

    </ns0:CustomerName>

     

    The script block does not support parameter arrays and any attempt to use them will result in the exception: System.Xml.Xsl.XslTransformException: Extension function parameters or return values which have Clr type 'String[]' are not supported. To overcome this add overload methods that take the desire number of parameters, concatenate the parameter into a comma separated string, and then pass the single parameter to the CallAzureFunction method.

  5. Upload the new XSLT map to the Azure Integration Account.

Configuring the Logic Apps XML Transform action

  1. Add an XML Transform action in the Logic App designer;

  2. Select the uploaded XSLT map. When this is selected an AzureFunctions parameter field will appear;

  3. Add a JSON string containing a list of Azure function names and URLs, e.g.:

     

    {"GetCustomerName": "https://tcdemos.azurewebsites.net/api/GetCustomerName/customerNum/{customerNum}?code=Hdofm0MAZUvJoid7etspBKKwWCxYiqdV212lVSo9dkA2xM8Dmg7FFw==",
     "Function2": "URL2",
     "Function3": "URL3"
    }
    

     

    In the example below the Azure function list was added to a compose action and the output of the action used for AzureFunctions parameter:

 

Pros: The Logic App XSLT Transform action can be used. Will work for any function.

Cons*: Involves changes to the BizTalk XSLT map. Extra Logic Apps actions. An Azure Function has warm up time and a Consumption Plan has no SLA.*

9. Summary

The preceding six sections addressed common problems faced when trying to migrate a BizTalk map to a Logic App Integration Account and subsequently using that map in an XML Transform action, namely the "category 2" maps defined in section 2. Part 2 will focus on those maps ("category 3") which require a solution where the Logic App XML Transform action will not suffice, which could be due to the complexity of the map or simply because the XML Transform action is not available because there no Integration Account subscription.


Part 2 >>