SOAP Header Extensions How-To
Passing non-standard information in SOAP headers
Introduction
Create a SoapExtension
that allows you to insert XML into a SOAP header.
Background
I work for a very large company with diverse technology solutions. My shop is .NET, but we're consuming a web service from another group that uses WebSphere and Java for their web services. When I tried to consume this web service from my ASP.NET application, I ran into some issues, because the web service expected a username and password security token, passed in the SOAP header. I assumed (gulp) that it'd be a small matter of programming (SMOP) to insert well-formed XML into a SOAP header. That's mostly right, but there is a complete dearth of documentation on how to do this. I'm from the Internets, and I'm here to help, folks.
Using the code
I'm consuming a Java web service from a WebSphere server. I have no problems creating the web reference and consuming the service, but apparently it expects more than what .NET sends it. Here's the header that comes out of my ASP.Net application:
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:tns="http://tempuri.org/"
xmlns:types="http://tempuri.org/encodedTypes"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
.
. Body stuff here, with appropriate XML, passing parameters and values to
the web service...
.
Here's what the web service expects:
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Header>
<Security>
<UsernameToken>
<Username>XXXXXXX</Username>
<Password>XXXXXXXXXXXXXXXXXXXX</Password>
</UsernameToken>
</Security>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
.
.Body stuff here, with appropriate XML, passing parameters and values to
the web service...
Let me be clear: this is an ASP.NET (C#) web form application, consuming a WebSphere web service. There is NO web method in the service for passing the username token. As I've illustrated above, the expectation is for this token (well-formed XML) to be inserted into the SOAP header. This is where the SoapExtension
comes to the rescue.
First, you need to create an extension class, one that inherits SoapExtension
. Something along the lines of:
public class myExtension : SoapExtension
By default, a SOAP extension has 5 overrides that you must provide: ChainStream
, GetInitializer
(2 of them), Initialize
, and ProcessMessage
. ProcessMessage
and ChainStream
are really the heart of the matter, and you'll see why in the code. ChainStream
provides an override to access the XML stream. ProcessMessage
provides an override for you to manipulate that stream. Inside ProcessMessage
, you'll need to test for the 4 stages of a SoapMessage
, namely .BeforeSerialize
, .AfterSerialize
, .BeforeDeserialize
, and .AfterDeserialize
. We're interested in what's being sent from the client to the server, so the SoapMessageStage
.AfterSerialize
test is where we get the XML from cache, convert it to a string, do our manipulations, convert the string back out to a stream, and send it on it's merry way. Once this class is built, you need to perform one more very important step - make that extension available to your application. I have a web application, so I put this into the web.config file:
<webServices>
<soapExtensionTypes>
<add type="bberry1.myExtension,bberry1" priority="1" group="Low"/>
</soapExtensionTypes>
</webServices>
</system.web>
Note that my namespace is bberry1
, and the extension class is the brilliantly named "myExtension
." Once that's done, and you've rebuilt your project, you call your webservice, your overrides kick in, and you get what you want. Next, the class is constructed. I've placed all the code in a file named "customfilter.cs" in my project.
I mentioned several overrides above, and pointed out that ChainStream
and ProcessMessage
were the crux of the issue. I've got a few declarations to make for this class first:
public class myExtension : SoapExtension
{
public bool outgoing = true;
public bool incoming = false;
private Stream outputStream;
public Stream oldStream;
public Stream newStream;
Next, I build the ChainStream
override:
public override Stream ChainStream(Stream stream)
{
// save a copy of the stream, create a new one for manipulating.
this.outputStream = stream;
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
This makes the stream available to us for use in ProcessMessage
. ProcessMessage
is where we see what phase of the whole SOAP stream we're in, and process accordingly. I can try to explain the gyrations here, but I think the code speaks for itself. One thing that bears explanation, however, is that I convert the outgoing stream to a string, add my stuff, then convert the string back out to the stream. This is all done in SoapMessageStage.AfterSerialize
. I still need for that stream to go back out to the web service, so I need to process that stream out down in SoapMessageStage.BeforeDeserialize
.
The process of converting the XML cache to string gave me quite the headache, as there was almost NO documentation on the Internets on how it's done (I'm a complete SOAP/XML newbie, mind you). As it turns out, it's pretty easy, and is done like this:
public string getXMLFromCache()
{
newStream.Position = 0; // start at the beginning!
string strSOAPresponse = ExtractFromStream(newStream);
return strSOAPresponse;
}
Here's the ExtractFromStream
function:
private String ExtractFromStream(Stream target)
{
if (target != null)
return (new StreamReader(target)).ReadToEnd();
return "";
}
Here's the ProcessMessage
override in it's entirety:
public override void ProcessMessage(SoapMessage message) { StreamReader readStr; StreamWriter writeStr; string soapMsg1; XmlDocument xDoc = new XmlDocument(); // a SOAP message has 4 stages. We're interested in .AfterSerialize switch (message.Stage) { case SoapMessageStage.BeforeSerialize: break; case SoapMessageStage.AfterSerialize: { // Get the SOAP body as a string, so we can manipulate... String soapBodyString = getXMLFromCache(); // Strip off the old header stuff before the message body // I'm not completely sure, but the soap:encodingStyle might be // unique to the WebSphere environment. Dunno. String BodString = "<soap:Body soap:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"; int pos1 = soapBodyString.IndexOf(BodString) + BodString.Length; int pos2 = soapBodyString.Length - pos1; soapBodyString = soapBodyString.Substring(pos1, pos2); soapBodyString = "<soap:Body>" + soapBodyString; // Create the SOAP Message // It's comprised of a <soap:Element> that's enclosed in <soap:Body>. // Pack the XML document inside the <soap:Body> element String xmlVersionString = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; String soapEnvelopeBeginString = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soapenc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:tns=\"http://tempuri.org/\" xmlns:types=\"http://tempuri.org/encodedTypes\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">"; String soapEnvHeaderString = "<soap:Header><Security><UsernameToken><Username>"; String soapEnvHeaderString2 = "</Username><Password>"; String soapEnvHeaderString3 = "</Password></UsernameToken></Security></soap:Header>"; Stream appOutputStream = new MemoryStream(); StreamWriter soapMessageWriter = new StreamWriter(appOutputStream); soapMessageWriter.Write(xmlVersionString); soapMessageWriter.Write(soapEnvelopeBeginString); // The heavy-handed part - forcing the right headers AND the uname/pw :) soapMessageWriter.Write(soapEnvHeaderString); soapMessageWriter.Write("usernamestring"); soapMessageWriter.Write(soapEnvHeaderString2); soapMessageWriter.Write("passwordstring"); soapMessageWriter.Write(soapEnvHeaderString3); // End clubbing of baby seals // Add the soapBodyString back in - it's got all the closing // XML we need. soapMessageWriter.Write(soapBodyString); // write it all out. soapMessageWriter.Flush(); appOutputStream.Flush(); appOutputStream.Position = 0; StreamReader reader = new StreamReader(appOutputStream); StreamWriter writer = new StreamWriter(this.outputStream); writer.Write(reader.ReadToEnd()); writer.Flush(); appOutputStream.Close(); this.outgoing = false; this.incoming = true; break; } case SoapMessageStage.BeforeDeserialize: { // Make the output available for the client to parse... readStr = new StreamReader(oldStream); writeStr = new StreamWriter(newStream); soapMsg1 = readStr.ReadToEnd(); xDoc.LoadXml(soapMsg1); soapMsg1 = xDoc.InnerXml; writeStr.Write(soapMsg1); writeStr.Flush(); newStream.Position = 0; break; } case SoapMessageStage.AfterDeserialize: break; default: throw new Exception("invalid stage!"); } } }
Points of Interest
What's good about this little exercise is that I found out how completely I can control the information flowing back and forth between my client and the web service. It's really nice to know that Microsoft provides theSoapExtension
, but it would've been nicer to have a how-to like this one as part of the documentation.
History
- Aug 6th, 2007: Initial Release