Inside of ChainStream()

Now that I've hand-coded a couple of non-trivial SOAP extensions, I can safely say that SoapExtension.ChainStream() is one of my most despised functions in the entire .NET framework. The worst part about it is that it's by and large undocumented -- from reading the various SoapExtension walkthroughs available elsewhere on the net, I've gotten the feeling that most authors don't want to spend any time on it because the semantics of the method are so bloody confusing. I couldn't really find some good documentation on it, so I fired up Reflector and went to town on the System.Web.Services.dll.

 

(Note that this analysis is based on the 1.1 framework).

 

SOAP extensions process message contents much the same way schoolchildren play “Telephone:” a message enters one end of the pipleline and gets passed from participant to participant. Along the way, each SOAP Extension is free to do whatever it wants to the message contents before passing the message along to the next link in the extension chain. By the time the message reaches its eventual destination (which is the XML Serializer for inbound messages and an HttpResponse object for outbound messages), it bears some resemblance to the message that came into the chain but may be substantially different.

 

SoapMessages are associated with their respective streams by the SoapServerProtocol class. Depending on whether the SoapMessage is a SOAP request or a SOAP response, SoapServerProtocol will initialize this stream differently. During the process of a servicing a SOAP Request, SoapServerProtocol initializes the message’s Stream property with the input stream of the HTTP Request that carried the SOAP request to the server from the client. During the processing of a SOAP response, the SoapServerProtocol class initializes the message’s Stream with a new instance of SoapExtensionStream. This SoapExtensionStream will eventually wrap the response stream associated with the outgoing HttpResponse object that will carry the SOAP response back to the client.

 

The SoapServerProtocol class is responsible for calling SoapMessage.InitializeStreamChain(). InitializeStreamChain() is called from two separate places in SoapServerProtocol: once during SoapServerProtocol.Initialize() to handle the servicing of the request, and once during SoapServerProtocol.WriteReturnValues to handle the servicing of the response. InitializeStreamChain() takes an array of concrete SoapExtension instances, one for each extension that will process the message. Regardless of whether a request or reponse is being serviced, the current value of the message’s Stream property will always be first link the chain. When InitializeStreamChain() completes, the message’s Stream property will contain a pointer to the stream returned from the last SoapExtension in the processing chain. As a result, the dispatch code implemented in other parts of the WebServicesHandler doesn’t need to know anything about the existence of SOAP extensions – all it has to do is read the request from SoapMessage.Stream and write the response to SoapMessage.Stream. SoapServerProtocol and the stream chaining mechanism take care of the rest.

 

The implementation of SoapMessage.InitializeStreamChain() looks something like the following:

 

internal void SoapMessageInitExtensionStreamChain(SoapExtension[] extensions)
{

 

int i;
if (extensions == null)
{

 

return;

}
for (i = 0; (i < extensions.Length); i++)
{

 

this.stream = extensions[i].ChainStream(this.stream);

}

}

 

Individual SOAP extensions insert themselves into this processing pipeline by overriding the implementation of SoapExtension.ChainStream(Stream stream). The default implementation does little, and looks something like this:

 

public virtual Stream ChainStream(Stream stream)

{
       //This basically does nothing.

       return stream;

}

 

Natively, this method doesn’t do much. It just returns the stream that was passed in, which doesn’t enable the extension to do much in terms of message processing. If the extension wants access to the contents of the message, it needs to cache a reference to the stream passed in to ChainStream() and return a new stream for other extensions to consume. A more useful implementation of ChainStream() looks like this:

 

public override Stream ChainStream(Stream stream)

{

       this.oldStream   = stream;

       this.newStream = new MemoryStream();

       return newStream;

}

 

Although this implementation of ChainStream() will work, it makes working with the chained streams more difficult than necessary. The reason for this has to do with when ChainStream() gets called by the framework during message processing. It’s actually called twice during the lifetime of a web method call – once just prior to the BeforeDeserialize phase, and once just prior to the BeforeSerialize phase. To compound the problem, the semantics of the oldStream and newStream variables are different depending upon which time ChainStream() is called. When ChainStream() is called prior to BeforeDeserialize, oldStream contains a readable stream containing the contents of the SOAP request and newStream will be empty, waiting to be filled up with the revised request. However, prior to BeforeSerialize, oldStream contains a writable stream representing where the response should go. The newStream stream will contain the contents of the response, having been previously populated by the framework or other SOAP extensions higher in the chain. It’s confusing, to say the least.

 

Caching references to both the incoming argument to ChainStream() and the stream returned from ChainStream() is sufficient to enable message interception and alteration. However, the naïve implementation makes message processing more difficult, because of the changing semantics of newStream and oldStream. A better implementation of ChainStream() abstracts processing code from this semantic shift by using some conditional logic to clarify the situation (BTW, I found the basics of this implemention somewhere on the Net, but I can't for the life of me remember where right now):

 

public override Stream ChainStream(Stream stream)

{

//This flag will be set to true during the AfterDeserialize

//handler in ProcessMessage()

       if( !bPostDeserialize)

       {

           //We’re deserializing the message, so stream is our input

    //stream

           this.inputStream = stream;

           this.outputStream = new MemoryStream();

           return outputStream;

       }

       else

       {

          //We’re serializing the message, so stream is the

   //destination of our bits

          this.inputStream = new MemoryStream();

          this.outputStrem = stream;

          return inputStream;

       }

}

 

Implementing ChainStream in this way allows code that actually does the message processing to always be able to read from this.inputStream and write to this.outputStream, regardless of where in the message processing cycle the code currently is.

 

As if there were not enough streams in the picture already, the SoapMessage object passed as an argument to ProcessMessage() will itself contain a reference to a stream, exposed via the public read-only Stream property. It’s tempting to use the contents of this stream for processing, but this approach will not work in all cases. SoapMessage.Stream always points to the last stream in the processing chain, and as such should never be written to or read from. Doing so could potentially circumvent other extensions and will most likely result in an empty request being passed to the consumer. The reason for this is that other SOAP extensions are going to take their input from the stream your extension returned from ChainStream() – which may or may not be the stream pointed to by the SoapMessage’s Stream property. If you write directly to the message’s stream, the stream your extension had originally returned from ChainStream() will have no contents and therefore pass no input to downstream chains. Assuming that downstream chains are implemented properly, the last extension in the chain will copy its input stream (which will be empty) to its output stream (which will be the same stream contained in the SoapMessage) – destroying the contents which your extension wrote there directly. In short, it’s best not to rely on the SoapMessage.Stream property at all, and only do operations on stream references your extension cached during the execution of ChainStream().

 

Once I understood what was going on with ChainStream(), the rest of the SOAP extension model made perfect sense and was actaully pretty straightforward to work with. Even ChainStream itself isn't so bad if you can get around the fact that the semantics of the method's argument are totally dependent on the call sequence. Looking back on my experience with SOAP extensions, I'm glad that Microsoft is moving to phase them out from public consumption. The functionality that SOAP extensions provide can be more easily accomplished using the richer progamming model of the WSE custom filters. And that, I think, is a definitively good thing.

#1 Steve Maine on 3.16.2006 at 10:14 AM

Karan -- Aaron Skonnard wrote a great article on doing schema validation with SoapExtensions: msdn.microsoft.com/.../Hope that helps.-steve

#2 Michael McGranahan on 5.11.2006 at 5:28 PM

I think another way of looking at the issue is that existing MSDN documentation is strictly at fault.Every MSDN article suggests the use of the names oldStream and newStream, or originalStream and workingStream.A much better naming abstraction would be wireStream and appStream, or externalStream and internalStream.

#3 DiegoV on 10.01.2006 at 9:06 PM

I am just passing trough this, 3 years after! Well, I felt the need to suggest you not to call the streams new/old, nor input/output, but rather network/extension, original/chainned, or whatever. Then you don't need any extra logic in ChainStream. Anyway Michael McGranahan already did. I think SoapExtensions are very simple and that is a very nice thing when compared with the current (non beta) alternatives. The only problem I have with them is how hairy it gets to inject them in the client side (I don't want to deal with type importers).