Capturing SOAP messages that serialization cannot handle
In my job I have to write the code which consumes third party web services. In the course of working with differing companies' web services I discovered that some of those would return invalid XML data. That data was invalid due to either from an outdated schema or had some subtle problem that would cause Microsoft's deserializer to throw an exception. Since I needed to see the data sent to me, I had to work around the de-serialization operation and access the raw SOAP message which Microsoft's code refuse to divulge to my web service. This article explains I was able to end-around Microsoft by creating a specific attribute tied to a method call that allows me to see the SOAP message before serialization and will log the message for access when needed.
Notes
- These steps are not how I coded it, but how you should proceed with the implementation.
- This article does not cover the whys or hows of C# web services. It only details the acquisition of SOAP data in exceptional circumstances.
- This problem occurred in a C# web service accessing a third party web service, hence the design taken below.
Step 1: Inserting a custom attribute into the proxy code
Since we are consuming a foreign web we either have to deal with Microsoft's generated proxy code or we have to create it by hand; regardless we need to find where the invoked call returns data. That is the location where we will place our specialized attribute.
If it is a generated proxy then you will need to look in the hidden file Reference.cs which resides in the web services folder. Once found, locate the primary method that returns data. That is the location were you will need to add the attribute that will handle the leg work of getting around the de-serialization situation.
[ClientSoapLogger()]
public zzzResponse ProcessData(...
{
object[] results = this.Invoke(...
return((zzzResponse)(results[0]));
}
Notes
- If this is the generated proxy code, you will need to add the attribute every time the code is re-generated.
- You will need to add the reference to the namespace that the attribute code resides in as shown in future steps. The build process will remind you if you forget.
Step 2: Defining custom attribute SoapExtensionAttribute
The attribute is the hook for our data logging process. Here is the class which is the attribute with the appropriate overrides.
[AttributeUsage(AttributeTargets.Method)]
public class ClientSoapLoggerAttribute :
SoapExtensionAttribute
{
private int m_Priority = 0;
public override Type ExtensionType
{
get { return typeof(ClientSoapLogger); }
}
public override int Priority
{
get { return m_Priority; }
set { m_Priority = value; }
}
}
Step 3: Creating the SoapExtension
The SOAP extension will work with the flow of the handling of SOAP message in an autonomous fashion. Our goal is not to damage the data stream, only to look into it for data extraction.
public class ClientSoapLogger : SoapExtension
{
public Stream oldStream;
public Stream newStream;
public override object GetInitializer(
Type serviceType)
{
return null;
}
public override object GetInitializer(
LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute)
{
return null;
}
public override void Initialize(object initializer)
{
return;
}
public override Stream ChainStream(Stream stream)
{
oldStream = stream;
newStream = new MemoryStream();
return newStream;
}
public override void ProcessMessage(SoapMessage message)
{
switch(message.Stage)
{
case SoapMessageStage.BeforeDeserialize:
LogResponse(message); break;
case SoapMessageStage.AfterSerialize:
LogRequest(message); break;
case SoapMessageStage.AfterDeserialize:
case SoapMessageStage.BeforeSerialize:
default:
break;
}
}
Step 4: Logging the data
This is the step that really got to me. If your code is in a web service (mine was a web service calling a web service) how does one pass data between the SOAP process and the code accessing the proxy? In some situations such as ASP.NET one can put the message on the HttpContext
, but that wasn't an option for my code in a web service. The below code handles the situation of a null HttpContext
by placing the data in the Remoting
call context.
public void LogResponse(SoapMessage message)
{
Copy(oldStream,newStream);
newStream.Position = 0;
string strSOAPresponse =
ExtractFromStream(newStream);
HttpContext hc = HttpContext.Current;
if (hc != null)
hc.Items.Add("SOAPResponse",
strSOAPresponse);
else
System.Runtime.Remoting.Messaging.CallContext.SetData(
"SOAPResponse",strSOAPresponse);
newStream.Position = 0;
}
private String ExtractFromStream(Stream target)
{
if (target != null)
return (new StreamReader(target)).ReadToEnd();
return "";
}
private void Copy(Stream from, Stream to)
{
TextReader reader = new StreamReader(from);
TextWriter writer = new StreamWriter(to);
writer.WriteLine(reader.ReadToEnd());
writer.Flush();
}
}
Step 5: Consuming the logged data
For my web service if the incoming data was invalid, my indication was either an exception thrown by the de-serializer or simply a null
return value by the data access method. At that point my code tries to do a post extract of the SOAP raw data and handles the message.
[Editor Note: Line breaks used to avoid scrolling.]
protected virtual void DiagnoseResponseProblem()
{
HttpContext hc = HttpContext.Current;
string SoapResponse = null;
string SoapRequest = null;
if (hc != null)
{
SoapRequest = hc.Items["SOAPRequest"].ToString();
SoapResponse = hc.Items["SOAPResponse"].ToString();
}
else
{
try
{
SoapResponse = System.Runtime.Remoting.Messaging.
CallContext.GetData("SOAPResponse").ToString();
SoapRequest = System.Runtime.Remoting.Messaging.
CallContext.GetData("SOAPRequest").ToString();
System.Runtime.Remoting.Messaging.CallContext.
FreeNamedDataSlot("SOAPResponse");
System.Runtime.Remoting.Messaging.CallContext.
FreeNamedDataSlot("SOAPRequest");
}
catch (System.Exception){}
}
...
}
Notes
- Let me know if there is possibly a better way or things could be improved.