1. Introduction

BizTalk Server uses the concept of Promoted Properties for message routing which the Messaging Engine routes based on their values and their entry in the MessageBox subscription tables. Promoted Properties are defined in a Property Schema (see OrderPropertySchema.xsd in Appendix A for an example). This schema is used to make the properties available at design time and, when the application is deployed, used to add entries into database tables. Each property in the message context is composed of three things, namely, a name, a namespace, and a value.

Looking a little more closely at what happens to an XML schema that has properties promoted into the message context a couple of things occur. First, a reference to the Property Schema is added in an xs:annotation node and, second, XPath expressions are added for each property to a separate xs:annotation under a root node (note: a schema may define more than one), for example:

As an aside, It is worth noting that the added reference to the property schema contains a b:imports node. This should not confused with an XML schema xs:import node; a schema that has for the former will not fail to upload to a Logic App integration account if the property schema is not already uploaded, whereas the latter will fail if the schema to import does not already exist in the integration account.

At this point one may ask the question why the need for a property schema for properties that are promoted; it seems superfluous. The property schema does define the property data type, which is difficult to extract from an XPath expression, but that could easily be added as an extra attribute within the xs:annotation node. The real use for the property schema, then, seems to be for defining properties to be promoted into a message context whose value is not contained within the message, and the property schema does distinguish between the two by allowing a different .NET type to be declared for each: MessageDataPropertyBase and MessageContextPropertyBase

From a developer standpoint one difference between these two 'types' of promoted properties - where the value is or is not contain within the message - is that the latter need to be coded for, either in a pipeline component or in an orchestration, whereas the former are simply declared in the XML schema itself, and so this is what most commonly occurs in practice (and one consistent where the development environment is, principally, a declarative paradigm): within BizTalk, messages are routed on values within the message promoted into the context by the developer and/or system and adapter values not contained within the message promoted into the context by the Messaging Engine.

An example of message body properties (‘OrderPropertySchema’ namespace) and non-message body properties (‘system-properties’ namespace) to the message context:

Another couple of generalisations can be made about promoted properties. Firstly, as can be seen in the previous image, the properties are either string or integer values. It is true that other data types can be promoted, and the promotion includes conversion from an XSD data type to a CLR data type (see: https://docs.microsoft.com/en-us/biztalk/core/promoting-properties), but the reality is that these other data types rarely make their way into the context as promoted properties. Secondly, a property can be considered as being either a class member (e.g. BTS.MessageType) or instance member (e.g. BTS.MessageID).

A final consideration to be borne in mind is that while it is sometimes convenient to equate Logic Apps with BizTalk, since we can compare an orchestration workflow to a Logic App workflow and the integration account will store BizTalk maps and schemas, this misses the fact that a core function of BizTalk Server is message routing, something that Logic Apps cannot perform on its own. There are a number of Azure services that provide messaging services (see: https://docs.microsoft.com/en-us/azure/event-grid/compare-messaging-services) and the one most comparable to BizTalk’s publish and subscribe architecture is Azure Service Bus. In other words, BizTalk can be thought of as: BizTalk = Logic Apps + Service Bus (or better still: BizTalk ≈ Logic Apps + Service Bus + Functions).

Note: This article assumes the reader has reasonable development knowledge of BizTalk Server message routing, property promotion and correlation.  

2. Solution Overview

Given the broad generalisations and assumptions in the previous section the following design choices can be made:

  • Treat all properties to be promoted as string values since integers are easily cast to string and any filter value equality test only results in a minuscule performance loss;

  • Ignore the property schemas since all the essential details of the properties to be promoted are already contained in the XML schema (now we’re treating all properties as strings);

  • Create a mechanism to promote properties when the value is not contain within the message; 

  • Keep the namespace prefix on the property name. Notice that in the xs:annotation node for a property there is a name attribute with the value “ns0:CustomerID” but in the image showing the context properties the prefix is missing (remember the three parts to a property). By keeping the prefix and adding the namespace URI (provided in the b:imports node) as an extra property allows the promoted properties to be defined as  IDictionary<string,string> and any filter property name collisions can be handled by adding a new equality predicate to the filter condition using the namespace promoted property;

The solution will make use of an Azure Function to examine the incoming message, find its schema, and then determine which properties need to be promoted. The Azure Service Bus provides a BrokeredMessage class with is the unit of communication between Service Bus clients. This class has a Properties property (of type IDictionary<string, object>) that allows a Service Bus Topic Subscription to filter upon.

3. Azure Service Bus & Redis Cache

Firstly, create a new Azure Service Bus resource in the Azure portal. Once this is done, go to the new resource and find the Primary Connection String as shown below:


Using Service Bus Explorer

  1. Install the Service Bus Explorer 3.0.4 on our dev machine (https://github.com/paolosalvatori/ServiceBusExplorer/releases);

  2. Review the documentation on how to use Service Bus Explorer as well as the video tutorials provided here: https://github.com/paolosalvatori/ServiceBusExplorer;

  3. In Service Bus Explorer create a New topic (biztalkdemo), Subscription (Orders), and Subscription Rule (MessageType=Order) and Filter as shown below:


Now create a new Azure Redis Cache resource in the Azure portal. See https://docs.microsoft.com/en-us/azure/redis-cache/cache-dotnet-how-to-use-azure-redis-cache for details on how to do this.

4. Property Promotion Function

Most of the functionality of this solution will be done within an Azure Function. The function will need to perform the following tasks:

  • Retrieve the XSD schema of the incoming message stored in the Logic App Integration Account;

  • For a given XSD, return a list of properties to be promoted and create a list of ‘system properties’ (e.g. BTS.MessageType);

  • Store the promoted properties list in a cache so as not to look up the XSD schema each time a message is received;

  • Create a mechanism that allows for custom properties to be promoted into the context;

  • Create a BrokeredMessage and send to the Azure Service Bus.

Create a New Azure Function

The steps required to perform this are as follows:

  1. Create a new Azure Function (C# HTTP trigger). A function like the one below should be created by the template:


  2. Create a new file project.json and upload the file to the root folder (i.e. the same folder as run.cx). Adding this file will cause the relevant NuGet packages to be loaded. The project.json file should contain the following:


     "frameworks": {


         "dependencies": {

           "Microsoft.Azure.Management.Logic": "3.0.0",

           "Microsoft.Rest.ClientRuntime": "2.3.10",

           "Microsoft.IdentityModel.Clients.ActiveDirectory": "3.14.2",

           "Microsoft.Azure.Services.AppAuthentication": "1.1.0-preview",

           "StackExchange.Redis": "1.2.1"






  3. In the  run.csx file add the following code (we will need to update the three values: subscriptionId, resourceGroup, integrationAccount, redisCacheConnectionString, serviceBusConnectionString, topicName  to match our Azure subscription ID and the name of the resource group and integration account, and redis cache):

#r "Newtonsoft.Json"
#r "Microsoft.ServiceBus"
using Microsoft.Azure.Management.Logic;
using Microsoft.Azure.Management.Logic.Models;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Rest;
using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StackExchange.Redis;
using System;
using System.Linq;
using System.Net;
using System.Text;
using System.Xml;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
    log.Info("C# HTTP trigger function processed a request.");
    string subscriptionId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    string resourceGroup = "Demos";
    string integrationAccount = "xxxxxxxxxxxxxxxx";
    string redisCacheConnectionString = "xxxxxxxxxxx.redis.cache.windows.net:6380,password=xxxxxxxxxxxxxxxxxxxx=,ssl=True,abortConnect=False";
    string serviceBusConnectionString = "Endpoint=sb://xxxxxx.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=xxxxxxxxxxxxxxx=";
    string topicName = "BizTalkDemo";
    string rootNodesXPath = "/*[local-name()='schema' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='element' and namespace-uri()='http://www.w3.org/2001/XMLSchema'][@name='{0}']";
    string promotedPropertiesXPath = "/*[local-name()='schema' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='element' and namespace-uri()='http://www.w3.org/2001/XMLSchema' and @name='{0}']/*[local-name()='annotation' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='appinfo' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='properties' and namespace-uri()='http://schemas.microsoft.com/BizTalk/2003']/*[local-name()='property' and namespace-uri()='http://schemas.microsoft.com/BizTalk/2003' and not(@distinguished='true')]";
    string importsXPath = "/*[local-name()='schema' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='annotation' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='appinfo' and namespace-uri()='http://www.w3.org/2001/XMLSchema']/*[local-name()='imports' and namespace-uri()='http://schemas.microsoft.com/BizTalk/2003']/*[local-name()='namespace' and namespace-uri()='http://schemas.microsoft.com/BizTalk/2003']";
    string response = String.Empty;
    string schemaName = String.Empty;
    // Load XML message
    string content = await req.Content.ReadAsStringAsync();
    XmlDocument xd = new XmlDocument();
    string messageType = string.Concat(xd.DocumentElement.NamespaceURI, "#", xd.DocumentElement.LocalName);
    // Check cache for message promoted properties
    ConnectionMultiplexer cm = ConnectionMultiplexer.Connect(redisCacheConnectionString);
    IDatabase cache = cm.GetDatabase(); 
    string properties = cache.StringGet(messageType); 
    if (properties == null)
        // Get LogicManagementClient object to access the Integration Account artefacts
        var provider = new AzureServiceTokenProvider();
        var token = await provider.GetAccessTokenAsync("https://management.azure.com/");
        var client = new LogicManagementClient(new TokenCredentials(token)) { SubscriptionId = subscriptionId };
        // Find XML message schema
        var schemas = client.Schemas.ListByIntegrationAccounts(resourceGroup, integrationAccount);
        foreach (IntegrationAccountSchema ias in schemas)
            if (xd.DocumentElement.NamespaceURI == ias.TargetNamespace)
                // Get schema promoted "MessageDataPropertyBase" properties
                XmlDocument xdias = new XmlDocument();
                // Check for root node (the schema may have more than one)
                XmlNode xn = xdias.SelectSingleNode(String.Format(rootNodesXPath, xd.DocumentElement.LocalName));
                if (xn != null)
                    // Get schema promoted properties
                    XmlNodeList xnlProps = xdias.SelectNodes(String.Format(promotedPropertiesXPath, xd.DocumentElement.LocalName));
                    XmlNodeList xnlImports = xdias.SelectNodes(importsXPath);
                    // Add any required message service class context properties here
                    XmlNode xnContext = xdias.CreateElement("contexts");
                    XmlNode xnNamespace = xdias.CreateElement("context");
                    (xnNamespace as XmlElement).SetAttribute("name", "BTS");
                    (xnNamespace as XmlElement).SetAttribute("value", "http://schemas.microsoft.com/BizTalk/2003/system-properties");
                    XmlNode xnMessageType = xdias.CreateElement("context");
                    (xnMessageType as XmlElement).SetAttribute("name", "BTS:MessageType");
                    (xnMessageType as XmlElement).SetAttribute("value", messageType);
                    // Cache schema promoted properties
                    var xnl = xnlProps.Cast<XmlNode>().Concat(xnlImports.Cast<XmlNode>()).Concat(xnContext.ChildNodes.Cast<XmlNode>());
                    properties = JsonConvert.SerializeObject(xnl);
                    cache.StringSet(messageType, properties);
    // Send message to Service Bus Topic
    using (BrokeredMessage bm = new BrokeredMessage(content))
        // Promote data and context properties
        JArray ja = JsonConvert.DeserializeObject<JArray>(properties);
        foreach (JToken jt in ja)
            string name = string.Empty;
            string value = string.Empty;
            string key = jt.First.Path.Substring(jt.First.Path.IndexOf(".") + 1);
            log.Info("key: " + key);
            switch (key)
                case "b:property":
                    name = (string)jt.SelectToken(key + ".@name");
                    string xpath = (string)jt.SelectToken(key + ".@xpath");
                    value = xd?.SelectSingleNode(xpath)?.InnerText ?? String.Empty;
                case "b:namespace":
                    name = (string)jt.SelectToken(key + ".@prefix");
                    value = (string)jt.SelectToken(key + ".@uri");
                case "context":
                    name = (string)jt.SelectToken(key + ".@name");
                    value = (string)jt.SelectToken(key + ".@value");
            bm.Properties.Add(name, value);
        // Promote message service instance context properties passed in the header from a message publisher
        IEnumerable<string> contextProperties;
        if (req.Headers.TryGetValues("contextProperties", out contextProperties))
            var joKVP = JObject.Parse(contextProperties.First());
            foreach (KeyValuePair<string, JToken> prop in joKVP)
                // Note this will either add the property or update the value if it exists
                bm.Properties[prop.Key] = (string)prop.Value;
        // Promote any message service instance context properties here
        bm.Properties["BTS:MessageID"] = Guid.NewGuid().ToString();
        // Create MessagingFactory, Service Bus NamespaceManager and send to Topic
        MessagingFactory mf = MessagingFactory.CreateFromConnectionString(serviceBusConnectionString);
        NamespaceManager nm = NamespaceManager.CreateFromConnectionString(serviceBusConnectionString);
        TopicClient tc = mf.CreateTopicClient(topicName);
    return req.CreateResponse(HttpStatusCode.OK, "success", "application/xml");  

Test the Azure Function

At this point, the Azure Function can now be unit tested.

  1. Upload the BizTalk XSD schemas provided in Appendix A to the Azure Logic App integration account;

  2. In the Integration Account Access control (IAM) grant the Azure Function access to the Integration Account:

  3. Create a Header value: contextProperties. Set the value to: {"TestMsg":"True"};

  4. Add the Order XML message given in Appendix B to the Request body;

  5. Run the test. It should result in a 200 response as shown below:

  6. Go to Service Bus Explorer, right-click the Subscription, click Receive Messages, and click OK (Peek Messages). A new Message tab should appear with details of the BrokeredMessage sent to the Service Bus. The Promoted Properties should appear in the Message Custom Properties window, as shown below:

5. Creating & Using a Correlation Set

In BizTalk Server the process of matching an incoming message with the appropriate instance of an orchestration is known as correlation. There are three correlated messages exchange patterns (for further details see: https://docs.microsoft.com/en-us/biztalk/core/using-correlations-in-orchestrations). An example of one of these patterns – sequential convoy – has already been published on TechNet (see: https://social.technet.microsoft.com/wiki/contents/articles/40255.enforcing-ordered-delivery-using-azure-logic-apps-and-service-bus.aspx), so there’s no need to repeat it, but basically by enabling ‘Enable Session’ on a topic subscription allows messages with the same SessionId to be routed to the same instance, and so the example uses an account number to process all the transaction messages for an account same Logic App instance. In BizTalk, however, it is possible to create a correlation type that consists of one or more promoted properties, e.g.:

So the issue is to define a SessionId that is valid for multiple properties, and it’s compounded by the fact that there’s an unhelpful constant defined in Microsoft.ServiceBus.Messaging:  MaxSessionIdLength = 128;. Further, there seems to be some restriction allowable characters.

One solution is to concatenate the values (and the property name too if space will allow) and then Base64 encode the string. So, “ns0:CustomerID=1357911;ns0:ShippingAddressID=24681012;” (55 characters) becomes “bnMwOkN1c3RvbWVySUQ9MTM1NzkxMTtuczA6U2hpcHBpbmdBZGRyZXNzSUQ9MjQ2ODEwMTI7” (73 characters), which will work fine so long as the property values will never increase the overall string length to more than 128 characters.

For a ‘correlation set string’ that would be greater than 128 an alternative would be to compute a hash value of the string which should provide sufficient entropy to avoid collisions. In both cases it should be noted that the order in which the properties are appended to the ‘correlation set string’ will affect the encoding/computed hash.

Going with the first option the following code can be added before the MessagingFactory object is created:

// Check for Correlation Set
IEnumerable<string> correlationSet;
if (req.Headers.TryGetValues("correlationSet", out correlationSet))
    string sessionIds = string.Empty;
    var jaCorrelation = JArray.Parse(string.Concat("[", correlationSet.First(), "]"));
    foreach (JArray jac in jaCorrelation)
        string sessionId = string.Empty;
        JArray jaS = new JArray(jac.OrderBy(e => e));    
        foreach (string prop in jaS)
            sessionId += string.Concat(prop, "=", bm.Properties[prop], ";");
        byte[] bytes = Encoding.UTF8.GetBytes(sessionId);   
        sessionIds += string.Concat(Convert.ToBase64String(bytes));
    bm.SessionId = sessionIds;


Test the Azure Function

At this point, the Azure Function can now be unit tested.

  1. Create a Header value: correlationSet. Set the value to: ["ns0:CustomerID", "ns0:ShippingAddressID"];

  2. Add the Order XML message given in Appendix B to the Request body;

  3. Run the test. It should result in a 200 response as shown below:


  4. Use Service Bus Explorer to check for messages. The SessionId property should be set in the Message Properties window, e.g.:



  5. Decoding the Base64 value (e.g. try https://www.base64decode.org) should yield the text: ns0:CustomerID=1357911;ns0:ShippingAddressID=24681012;
  6. Modify the correlationSet value to ["ns0:ShippingAddressID", "ns0:CustomerID"] (i.e. reverse the order) and re-run the test. It should produce the same SessionID value.


6. Some Further Considerations

There are a few things to bear in mind when using this method of Property Promotion in Azure Service Bus:

  • The function will use the first schema that matches the namespace#rootnode message type. If multiple versions of the same schema are deployed the function will need to be modified to cater for such a  scenario.
  • By copying the  promotedPropertiesPath  variable and removing them and not(@distinguished='true') any distinguished fields in the XML schema can be retrieved and subsequently added to the context, which can then be used for routing. In BizTalk, promoting a large number of properties would affect performance and not a best practice, but in Azure Service Bus this is not the case;
  • The Promoted Property types are all declared as strings. If the data type of the promoted property in the schema is required, then there are a couple of options:
    1. The data type could be added as a new attribute to the b:property node in the XSD schema;

    2. The location attribute of the b:imports node is available to determine the name of the property schema. This value can be used to a LogicManagementClient object, i.e.

IntegrationAccountSchema schema = client.Schemas.Get(resourceGroup, integrationAccount, schemaName);

to retrieve the property schema and then the property data type searched for in the schema. 

  • Message 'class member' properties could also be added to an XML Schema's meta-data in the Logic App integration account (select the XML Schema, click Edit as JSON, and locate the "metadata" name). So, for example, supposing it was decided that for certain messages (e.g. orders, invoices, refunds etc.) they would be archived for a certain period. Now one way would be to add a new subscription and filter on BTS.MessageType. However, an alternative would be to add an "archive": "true" to the meta-data. The function could then be modified to read the metadata when then the schema is loaded, store any properties found and then promoted them in the BrokeredMessage. The new archive subscription then would simply filter on user.Archive='true'.
  • The current solution works for a single Correlation Set since it is set as the SessionId, which can be considered the equivalent of an Orchestration InstanceId. Routing a message that has promoted properties that fulfill multiple correlation sets is difficult without maintaining a table of Correlation Sets to SessionId mappings.

One possible solution would be to send a copy of the BrokeredMessage (different MessageId) to the Service Bus for each additional Correlation Set. One issue with this approach would be duplicate messages being received by any topic subscriptions that filter on message class member, for example BTS.MessageType. However, this could be prevented by introducing and setting a Duplicate property that could be added to such subscriptions.

  • See Appendix C to better understand what gets stored in the Redis Cache for a given XML schema.

7. Summary

This purpose of this article is to provide a mechanism for routing XML messages, of BizTalk Server schemas uploaded to an Azure Logic App’s integration account, using their promoted properties. And while not being an exact implementation of the BizTalk pub/sub model it is designed to be a close match for many BizTalk implementations. It also provides means of using simple correlation to route messages to a Logic App instance for implementing a convoy pattern.  The Azure Function’s behaviour can be roughly compared to that of the BizTalk Server XMLDisassembler component and can serve as a base to be extended or customized to meet the needs of an individual implementation.

8. References

How to use Azure Managed Service Identity (public preview) in App Service and Azure Functions:

https://docs.microsoft.com/en-us/azure/app-service/app-service-managed-service-identity Jump


Use Resource Manager authentication API to access subscriptions:

https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-api-authentication Jump


Microsoft.IdentityModel.Clients.ActiveDirectory Namespace:

https://docs.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.clients.activedirectory?view=azure-dotnet Jump


Getting started with the Logic Apps SDK:

http://www.bizbert.com/bizbert/2015/10/22/GettingStartedWithTheLogicAppsSDK.aspx Jump


LogicManagementClient class:

https://docs.microsoft.com/en-us/python/api/azure.mgmt.logic.logicmanagementclient?view=azure-python Jump


In-order delivery of correlated messages in Logic Apps by using Service Bus sessions:



Correlation Sets:



Accessing BizTalk Schema Distinguished Fields in the Azure Logic Apps Designer:



9. Appendix A: Schemas Files



<?xml version="1.0"?>

<xs:schema xmlns="PropertyPromotionsDemo.Order" xmlns:b="http://schemas.microsoft.com/BizTalk/2003" xmlns:ns0="OrderPropertySchema" targetNamespace="PropertyPromotionsDemo.Order" xmlns:xs="http://www.w3.org/2001/XMLSchema">




        <b:namespace prefix="ns0" uri="OrderPropertySchema" location=".\OrderPropertySchema.xsd" />




  <xs:element name="Order">




          <b:property name="ns0:CustomerID" xpath="/*[local-name()='Order' and namespace-uri()='PropertyPromotionsDemo.Order']/*[local-name()='CustomerID' and namespace-uri()='']" />

          <b:property name="ns0:ShippingAddressID" xpath="/*[local-name()='Order' and namespace-uri()='PropertyPromotionsDemo.Order']/*[local-name()='ShippingAddressID' and namespace-uri()='']" />






        <xs:element name="OrderID" type="xs:string" />

        <xs:element name="ItemName" type="xs:string" />

        <xs:element name="ItemCode" type="xs:string" />

        <xs:element name="Price" type="xs:string" />

        <xs:element name="Quantity" type="xs:string" />

        <xs:element name="CustomerID" type="xs:string" />

        <xs:element name="ShippingAddressID" type="xs:string" />







<?xml version="1.0"?>

<xs:schema xmlns="OrderPropertySchema" xmlns:b="http://schemas.microsoft.com/BizTalk/2003" targetNamespace="OrderPropertySchema" xmlns:xs="http://www.w3.org/2001/XMLSchema">



      <b:schemaInfo schema_type="property" xmlns:b="http://schemas.microsoft.com/BizTalk/2003" />



  <xs:element name="CustomerID" type="xs:string">



        <b:fieldInfo propertyGuid="7527ddbe-2cd2-4929-88c6-7b18bfea926e" propSchFieldBase="MessageDataPropertyBase" />




  <xs:element name="ShippingAddressID" type="xs:string">



        <b:fieldInfo propertyGuid="408fb92d-754a-44e4-a6e7-5a9d6dff461a" propSchFieldBase="MessageDataPropertyBase" />






10. Appendix B: Sample Message Files



<ns0:Order xmlns:ns0="PropertyPromotionsDemo.Order">


  <ItemName>Microsoft Surface Pro</ItemName>







11. Appendix C: Redis Cache Contents


Contents of the Redis cache using Redis Desktop Manager (see https://github.com/uglide/RedisDesktopManager/releases):