I’ve been writing some Pipeline Components for BizTalk 2006, and I ran into a bit
of a problem while testing one of them. The problem is a fairly common, but obscure
issue that appears with most non-trivial pipeline components: Stream Handling.
Charles Young has a pretty good article here about
the problems that appear when pipeline components don’t handle message stream correctly.
In particular, Charles talks about a bug in BizTalk 2004 which caused inbound maps
to fail when a pipeline component returned a seekable stream, and thus recommends
pipeline component developers always make sure they return a non-seekable stream to
avoid the issue.
My case, however, was a little bit different. It seems that Charles recommendation
only holds true for pipeline components that run in or after the disassemble
stage in a receive pipeline. I was, instead, writing a decoding component, and, in
my test, the receive interchange kept failing with the following message in the machine’s
Application Event Log: “Stream does not support seeking”. Indeed I was returning a
non-seekable stream from my pipeline component, but I haven’t figure out exactly why
this is an issue.
As far as I can see, this is related to the presence of the Xml Disassembler component
in the disassemble stage of the receive pipeline I was using to test my component.
Though BizTalk unfortunately doesn’t give you any details about the error (not even
a stack trace), it’s pretty hard to diagnose exactly what the problem is with non-seekable
streams here, but I think it has something to use with the use of the MarkableForwardOnlyEventingReadStream
used by the Xml Disassembler, which is constructed during message probing (IProbeMessage.Probe()).
I did some spelunking around doing some tracing and using Reflector, and it does indeed
seem that this special stream class that the disassembler creates called my Stream’s
CanSeek method (which properly returned false), but I could not see code where explicitly
an error message was returned or processing was halted based on this value, and instead
some alternative work was done, which I found rather confusing.
These kind of issues can be quite a bit of stressful because the semantics expected
by BizTalk (or rather, but the standard components included in BizTalk) are not documented
anywhere, making the process of writing a good and efficient pipeline component a
trial-and-error process, which is wildly frustrating and a very innefficient process
itself.
I think this is pretty nasty requirements for decoding component writers, as, in general,
they are usually built to decode a stream start to finish without moving around the
stream (most encryption/decryption components are easy, natural and efficient to write
this way). Additionally, it makes it imposible to simply have a decoding component
that returns a System.Security.Cryptography.CryptoStream stream to BizTalk, because
it is always non-seekable.
Solving the Issue
The easiest way to work around this problem would be to have the decoding component
decode the entire message into a seekable in-memory stream, but this is highly inneficient,
and it is sure to cause performance and scalability problems with either large interchanges
or a lot of concurrent messages flowing through the system.
What’s more frustraring about this is that it seems that the BizTalk Developers were
aware of this problem and solved it. After quite a bit of looking around, I discovered
that the MIME/SMIME component included in BizTalk is apparently aware of the problem
with non-seekable streams! It verifies if the data stream coming off the adapter is
seekable, and, if not, wraps it around a custom stream that allows read-only, seekable
access to it in what looks like a more efficient way than just loading it entirely
into memory. It basically does something like this:
if ( !stream.CanSeek
)
{
part.Data = stream = new ReadOnlySeekableStream(stream);
}
The ReadOnlySeekableStream class is implemented in Microsoft.BizTalk.Streaming.dll,
and is a public class. Unfortunately, it is undocumented, and the assembly is only
installed to the GAC, making it, at the very least, cumbersome and risky to reuse
on your own.
If anyone has any explanations of why seekable streams appear to be required on this
case, I’d sure would like to hear about it!
>