Following to my previous post about the proposed alternatives in creating XML message instances, I would like to finish the topic by giving you an overview on the solution that uses some internal BizTalk APIs for generating new messages and populating them with some initial data.
Depending on the particular requirements, a blank "XML skeleton" that contains no user data may hardly be called as usable message instance. Would it be nice to be able to initialise the new XML document with some initial values that we could either define inside the XSD schema or supply programmatically. Or would it be much better if we can use a set of XPath expressions in order to locate the nodes and set their values? Could we do it without loading the whole message into XML DOM? Well, let's see if some undocumented BizTalk APIs could assist with that...
As I described before, a message instance generated by the CreateXmlInstance method will already contain all elements and attributes in accordance with the corresponding XSD schema. There will be a placeholder for each element and attribute so if the schema has been declared with promoted properties or distinguished fields, you can start populating the message with the data inside a Message Assignment shape. The property assignment operations would be considered as an easiest mechanism to enrich your messages with the initial information, however from time to time it may not be possible to use a distinguished field (or a promoted property where appropriate) for value assignment purposes. For instance, there could be a repetitive node declared in the schema for which you will not be able to add a distinguished field declaration but you may still want to create a single instance of this repetitive element and configure some of its attributes.
In some (rare) scenarios, you might have been using the default values that you defined in the XSD schema. Luckily, the CreateXmlInstance method does a good job behind the scenes in order to recognise the XSD's default property on xs:element or the fixed property on xs:attribute and use the specified values when generating a new message instance. That is another simplest way to supply the initial values but it obviously limits you to a set of static data hardcoded in the schema so I wouldn't consider this approach as truly useful. However, it is always good to know about all available options to have a choice when a need arises.
The next option is rather much more powerful than any of those which have been covered so far. It is based on the assumption that you can construct a collection of valid XPath expressions pointing to various XML elements or attributes in the message instance. I will demonstrate a simple but really nice technique in managing XPath queries in the BizTalk projects, but this would certainly be a reason for another post. In the meantime, it is envisaged that a set of XPath queries will be supplied at run-time along with the initial values for the referenced XML nodes. The true magic is hidden inside the XPathMutatorStream class from the Microsoft.BizTalk.Streaming namespace. It wraps the XML data stream or XmlReader into a buffered stream the content of which "mutates" (in other words, gets translated into a possibly different XML stream) while it is being read by the consumer. The class implements the basic XPath streaming to apply the specified XPath queries against the XML infoset while the data is being passed through the underlying XML reader. In the event of finding a match, the class will invoke a callback method that is responsible for returning a final value of the XML entity (attribute or element) matching the XPath expression. The delegate signature for the callback method is the following:
/// <summary>
/// Defines a callback method that will be invoked by the XPathMutatorStream class when a match against any of the specified
/// XPath expressions has been found in the underlying XML stream.
/// </summary>
/// <param name="matchIdx">The index of the matched XPath expression in the collection supplied when initializing a XPathMutatorStream.</param>
/// <param name="matchExpr">The instance of the XPathExpression class containing the XPath query for which a match has been found.</param>
/// <param name="origVal">The original content (or value) of the matched XML element or attribute.</param>
/// <param name="finalVal">The new content (or value) that will be assigned to the matched XML element or attribute.</param>
public delegate void ValueMutator(int matchIdx, XPathExpression matchExpr, string origVal, ref string finalVal);
In order to help the XML stream to "mutate on fly" accordingly to our expectations, we need to construct a collection of XPath-to-Value mappings that is in fact a simple string dictionary where the key represents an XPath query and the value is the data that will be assigned to the matched XML element or attribute. This would also serve the purpose of defining the value assignment rules:
IDictionary<string, string> xpathToValue = new Dictionary<string, string>();
xpathToValue.Add("/*[local-name()='HwsMessage']/HwsSection/TaskID", Guid.NewGuid().ToString());
xpathToValue.Add("/*[local-name()='HwsMessage']/HwsSection/TaskDescription", "New task description goes here");
In the above example, I have used a generic Human Workflow Services task and created a value assignment rule for 2 fields (TaskID and TaskDescription). In essence, the rule is no more than just an XPath pointing to the target XML node and the new value for that node. The next step is to convert the XPathToValue dictionary to the Microsoft.BizTalk.XPath.XPathCollection type. Unfortunately, the only way to implement that is to enumerate through all items in the XPathToValue dictionary. We also need to build an array of values to be returned from the ValueMutator delegate as it generally provides better index-based lookup performance than a hash-based lookup through the dictionary:
// Construct and populate a new XPathCollection by enumerating through all entiries in the xpathToValue dictionary.
XPathCollection xc = new XPathCollection();
IEnumerator<KeyValuePair<string, string>> valueEnumerator = xpathToValue.GetEnumerator();
// An array of new values copied from the xpathToValue dictionary.
string[] values = new string[xpathToValue.Count];
int valueIndex = 0;
// For each iteration, add the key (XPath expression) to the XPathCollection and insert the value to the array.
while (valueEnumerator.MoveNext())
{
xc.Add(valueEnumerator.Current.Key);
values[valueIndex++] = valueEnumerator.Current.Value;
}
Now that we have constructed XPathCollection, the final step would be to pass the collection to an instance of the XPathMutatorStream class along with a valid XmlReader that will be used to fetch the source XML data. The support for anonymous methods in C# 2.0 greatly assists with providing an in-line implementation of the required delegate type:
XPathMutatorStream mutator = new XPathMutatorStream(xmlReader, xc,
delegate(int matchIdx, XPathExpression expr, string origValue, ref string finalValue)
{
// matchIdx provides the index of the matched XPath expression in the supplied XPathCollection.
if (matchIdx >= 0 && matchIdx < values.Length)
{
// The array of target values is laid out in the same order as XPathCollection so we simply use the matchIdx parameter as a index in the
// array to return a new value for the XML node (element or attribute) matching the XPath expression.
finalValue = values[matchIdx];
}
else
{
throw new ArgumentOutOfRangeException("matchIdx", matchIdx, "The specified parameter is not in the valid range.");
}
});
All we need to do in order to see the updated XML data is just to use the "mutation" stream and load its content into a new XmlDocument which couldn't be easier than the following:
XmlDocument msgInstanceWithData = new XmlDocument();
msgInstanceWithData.Load(mutator);
It is now time to sum up the discussion above and to come to the following conclusions:
- The CreateXmlInstance method allows you to generate a blank instance of the XSD schema effectively enabling you to consistently reuse your BizTalk schemas for message instantiation purposes;
- You can enrich the generated message instances with some initial data either by using the default or fixed declarations in the XSD schema, direct assignment via distinguished fields or streaming-based mutation;
- The distinguished field assignment would generally be considered as the preferred method for initializing the simple type, single-instance XML nodes from inside the BizTalk orchestrations;
- The functionality provided by the XPathMutatorStream class would more suit the custom components or where distinguished field cannot be adequately declared;
- The easiest way to supply the initial values for the XML nodes would be the definition of the XPath-to-Value mappings that combine the XPath queries to locate the target node and the new content for that node.
And finally, you can now download the sample code that I've pulled together for this article to visualise an example of the outlined technique in action.
Thanks & regards,
Valery