SoapMessages are associated with their respective streams by the SoapServerProtocol class. Depending on whether the SoapMessage is a
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
The implementation of SoapMessage.InitializeStreamChain() looks something like the following:
internal void SoapMessageInitExtensionStreamChain(SoapExtension extensions)
public virtual Stream ChainStream(Stream stream)
//This basically does nothing.
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();
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
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()
//We’re deserializing the message, so stream is our input
this.inputStream = stream;
this.outputStream = new MemoryStream();
//We’re serializing the message, so stream is the
//destination of our bits
this.inputStream = new MemoryStream();
this.outputStrem = stream;
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
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.
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.