Geeks With Blogs

News
Charles Young
Last week, I spent some time looking at a problem with a customer's orchestration.   In one place, the developer had made extensive use of the infamous BizTalk xpath() function.   This is not a true function, but a rather problematic feature of the XLang/s language that is often the cause of much pain and suffering to BizTalk developers. My advice, before I go any further, is to avoid using it and instead operate directly on the content of your messages using the XML processing features of the .NET Framework.
I've blogged about xpath() before (see http://geekswithblogs.net/cyoung/archive/2006/12/12/100981.aspx).   My chief complaint is that its behaviour is, as Yossi Dahan put it somewhere, 'context-sensitive'. The orchestration compiler (really a C# code generator) inspects the type of the target variable, field, property or parameter to which the results of the xpath() pseudo function will be assigned, and generates code which passes that type out to a method in BizTalk.   The type has a profound effect on the behaviour of the code at runtime.   This behaviour is not documented and very opaque.
The orchestration code made a number of calls to xpath().   Each call passed in a parameterised XPath string and assigned the resulting value to a distinguished field in a constructed message.   Every call worked fine, except the last one.   The one obvious difference between this last call and all the others is that it assigned the results to a field typed as 'xs:decimal' in the schema.
We were working against a deadline, and there wasn't enough time to fully investigate the behaviour we saw.   Instead, a work-around was found and implemented.   However, a couple of days later I sat down and reproduced the issues.   Sure enough, the use of the decimal type is problematic.   The reason is quite obscure.
Let's start looking at some peculiar behaviour that occurs with other data types.   I'll use a 'double'.   Consider a BizTalk orchestration message called inMsg with an XML payload that is structured as follows:
<Root>
 <Record>
    <DoubleValue>10.5</DoubleValue>
 </Record>
<Root>
To keep things simple, I've not used any namespace.   Say we attempt to use the following XPath with the xpath() function to assign the value of 10.5 to some distinguished field that is typed as a double.
outMsg.DoubleField = xpath(inMsg, "/Root/Record/DoubleValue");
This simple example fails!   The error will be logged as "There is an error in the XML document."   We will see later that this unhelpful message is technically correct, but not in the way you might imagine.    There is, of course, nothing intrinsically incorrect with either the XML document or the XPath.
BizTalk developers have discovered, through trial and error, that the workaround to this problem is to enclose their XPaths in a function such as string() or number().   The following call to xpath() works:
outMsg.DoubleField = xpath(inMsg, "number(/Root/Record/DoubleValue)");
The important difference here is that the first version of the XPath returns a node set, while the second version returns a value.   Inside The Microsoft.XLANGs.Core.Part class, these two XPaths result in the code taking different paths.   Everything works fine for the second case.   The code realises that the return type is not a node set and tests both the type of the returned value and the target type of the distinguished field to ensure that they are both 'basic'.   A basic type is a .NET built-in value type, a string or a DateTime.   If both types are 'basic', the code converts the value returned by the xpath() function to the type of the target distinguished field, and all is well.
In the case of the first XPath, things went wrong.   The code realised that the XPath returns a node set.   It iterates through the node set and creates an instance of a private class called ArrayBasedXmlNodeList which is derived from XmlNodeList.   It then tests to see if the target type is an XmlNodeList.   In this case, of course, the target type is a double.   The code therefore digs a little deeper.   Having determined that the ArrayBasedXmlNodeList list is not a 'basic' type it casts the list to an XmlNodeList, checks that it contains a single node and tests that node to see if it is of the same type as the target distinguished field.   Again, the test fails because the node is an XmlElement.
So far, so good.   The mechanism I have described supports the assignment of xpath() results to target node lists and XML nodes.   However, we want to assign to a double.  Having failed to match the source and target types, the code now attempts to handle this by deserialising the contents of the node.   It uses a helper method to get an instance of the .NET XmlSerializer class, assigns the node to an XmlNodereader and then attempts to deserialise the contents of the reader.   It is this call that throws the error.
The fundamental problem lies in the .NET XmlSerializer class itself.   The XmlSerializer implements explicit support for SOAP encoding. It supports the encoding rules often referred to as 'section 5 encoding’.   There are differences between the encoding rules for SOAP 1.1 and SOAP 1.2.   When using XmlSerializer, you have to specify explicitly which variation of the encoding style to use by passing a version-specific SOAP URI to the Deserialize method.   If you don't pass a URI, the XmlSerializer class defaults to an undocumented behaviour.   This is exactly what is happening when the XmlSerializer is used in the context of the BizTalk xpath() function.   BizTalk Server does not make any assumptions about encoding rules.   It does not pass a SOAP URI, and ends up invoking the undocumented encoding approach implemented within XmlSerializer
The undocumented approach adopted by XmlSerializer is to use an encoding style in which values are enclosed in an element, or assigned to an attribute, whose name directly reflects the type.   If an element is used, it must exist within the ‘global’ XML namespace.   For our 'double' value, the XmlSerializer therefore expects the inMsg content to be:
<Root>
 <Record>
    <DoubleValue><double>10.5</double></DoubleValue>
 </Record>
<Root>
Because the XmlSerializer doesn't find the <double> element, it throws an error stating the "there is an error in the XML document."   The XML document is well-formed, but is not valid in accordance with the encoding style expectations of the XmlSerializer.
Let's now consider the case where the target distinguished field is typed as a 'decimal' rather than a 'double'.   We can amend the XML document as follows:
<Root>
 <Record>
    <DoubleValue><decimal>10.5</decimal></DoubleValue>
 </Record>
<Root>
The following assignment will now work:
outMsg.DecimalField = xpath(inMsg, "/Root/Record/DoubleValue");
Unfortunately, in many scenarios, we can't change schemas to support this encoding style.   In this case, we might expect that we can simply enclose our XPath in a number() method, as before:
outMsg.DecimalField = xpath(inMsg, "number(/Root/Record/DoubleValue)");
This worked for ‘double’.   However, for ‘decimal’, this approach fails.   We get a new error stating that the application is "Unable to cast object of type 'System.Double' to type 'System.Xml.XmlNodeList'".     What is going on?
The root problem is that 'System.Decimal' is not a .NET built-in value type.   Specifically, the .NET CLR does not define a metadata code for this type, and hence there is no equivalent value for System.Decimal in the CorElementType enumeration.   This, in turn, means that when processing an xpath() function, BizTalk Server does not recognise the Decimal type as a 'basic' type. The BizTalk code contains a logic error.   It assumes that if either the target type or the return value of the xpath() function are not 'basic' types, then the xpath() function must have returned an XML node list.   This is incorrect logic.   In our case, the xpath() function returns a basic type (a double) thanks to the use of the 'number()' method.   However, the code sees that the target distinguished field is not a 'basic' type (it is, is fact, a System.Decimal) and assumes that the xpath() function must have returned an XML node list.  It tries to cast the double value to a node list and…bang!
So, there you have it. I have spent most of my BizTalk career avoiding the use of the BizTalk xpath() function.   In my opinion, it simply isn't worth the trouble it causes.   Not only is it unintuitive and opaque, its implementation suffers from logical errors.
What about the workaround? Well, the following works:
tempString = xpath(inMsg, "string(/Root/Record/DoubleValue)");
outMsg.DecimalField = System.Convert.ToDecimal(tempString);
Another approach is to pass orchestration messages out to custom helper code and do all your XPath-based processing there.
Posted on Tuesday, February 23, 2010 3:09 PM | Back to top


Comments on this post: BizTalk Server: Handling decimal types with the xpath() Function

# re: BizTalk Server: Handling decimal types with the xpath() Function
Requesting Gravatar...
Charles,
System.Convert.ToDecimal(tempString) could throw exception for the Nonnumeric values where as number() xpath function would just return "NaN" which is easy to handle. Thanks.
Left by Tamil on Aug 19, 2010 2:04 PM

# re: BizTalk Server: Handling decimal types with the xpath() Function
Requesting Gravatar...
Sure, but the issue is that when you try to use number() in the above example (with a decimal target variable), you run headlong into the logical bug and get the error I described. Internally, xpath() will try to cast the NaN to an XML node list, which won't work.

But yes, my workaround code is certainly not 'production ready'. Additional exception handling is required.
Left by Charles Young on Aug 26, 2010 2:10 AM

# re: BizTalk Server: Handling decimal types with the xpath() Function
Requesting Gravatar...
Thanks for good post

Im trying to set a value(long) in a message using xpath.

My statement look like this but it dont work

xpath(Message_Response,"number(/*[local-name()='ProcessPayment' and namespace-uri()='http://SigmaCommon.ProcessPayment']/*[local-name()='IndbetalingsID' and namespace-uri()='']/text())") = System.Convert.ToInt64(tmp_indbetliangsID);

Tried with and without the /text() in the end. No difference

I can read the value from the node before i try update so i suspect my xpath path is valid.

Get the erroe mesage: Expression must evaluate to a node-set.
Left by Jeppe on Jan 17, 2011 10:43 AM

Your comment:
 (will show your gravatar)


Copyright © Charles Young | Powered by: GeeksWithBlogs.net