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:
="1.0"="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)
{
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;
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();
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
break;
case SoapMessageStage.AfterSerialize:
{
String soapBodyString = getXMLFromCache();
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;
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);
soapMessageWriter.Write(soapEnvHeaderString);
soapMessageWriter.Write("usernamestring");
soapMessageWriter.Write(soapEnvHeaderString2);
soapMessageWriter.Write("passwordstring");
soapMessageWriter.Write(soapEnvHeaderString3);
soapMessageWriter.Write(soapBodyString);
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:
{
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 the
SoapExtension
, 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
Sr. Director at Epsilon, a leader in marketing technology.