Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / IIS

Extract raw SOAP Message returned from Foreign Web Service after Deserialization Failure in a Web Service

4.89/5 (12 votes)
11 Aug 20053 min read 4  
Local web service calls third party web service which returns XML data and serialization fails, the article shows how to obtain the actual SOAP message.

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.

C#
// Inside proxy class before data extraction call:
// Here is our attribute to capture 
// soap and log it for us.
[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.

C#
/// <summary>The attribute [ClientSoapLogger()] 
/// will appear in the data return method found in the
/// proxy code.</summary>
[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; }
   }
} // End of class ClientSoapLoggerAttribute

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.

C#
/// <summary>This class handles the data 
/// during the differing states and 
/// does so autonomously; ie in the background 
/// without affecting standard processing..</summary>
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;
   }

   /// <summary>Here is where we take a 
   /// dip into the stream. Due to the
   /// nature of streams if we dipped 
   /// without copying, we would damage the
   /// stream.</summary>
   public override Stream ChainStream(Stream stream)
   {
      oldStream = stream;
      newStream = new MemoryStream();
      return newStream;
   }

   /// <summary>With this override we can 
   /// access the data during different stages
   /// of processing. Logging occurs during the 
   /// before and after serialization.</summary>
   public override void ProcessMessage(SoapMessage message)
   {
      switch(message.Stage)
      {
         case SoapMessageStage.BeforeDeserialize: 
                                LogResponse(message); break;
         case SoapMessageStage.AfterSerialize:    
                                LogRequest(message);  break;

         // Do nothing on other states
         case SoapMessageStage.AfterDeserialize:
         case SoapMessageStage.BeforeSerialize:                    
         default:
            break;
      }
   }
// Class continued in step 4

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.

C#
/// <summary>Publish out the message received 
/// either in the HttpContext for ASP pages
/// or publish via remoting for webservices 
/// and winform applications.</summary>
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;

}

/// <summary>Transfer the text from the target 
/// stream from the current position.</summary>
private String ExtractFromStream(Stream target)
{
   if (target != null)
      return (new StreamReader(target)).ReadToEnd();

   return "";
}

/// <summary>Facilitate the copying process.</summary>
private void Copy(Stream from, Stream to)
{
   TextReader reader = new StreamReader(from);
   TextWriter writer = new StreamWriter(to);
   writer.WriteLine(reader.ReadToEnd());
   writer.Flush();
}
} // End ClientSoapLogger class

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.]

C#
// New class outside ClientSoapLogger...

/// <summary>Examine the soap return for possible missing 
/// data since the return was null.</summary>
protected virtual void DiagnoseResponseProblem()
{
   HttpContext hc = HttpContext.Current;
   string SoapResponse = null;
   string SoapRequest  = null;

   // Post processing debugging pull out 
   // the actual soap message and debug it.
   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");
      }
      // Ignore...most likely this system does not 
      // have the remote message context setup.
      catch (System.Exception){} 
   }
   ...
}

Notes

  • Let me know if there is possibly a better way or things could be improved.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here