Introduction
Some time ago, I had to add tracing of outgoing SOAP messages to our Web-Service. Tracing had to be applied only for certain web-calls, depending on the configuration. When I read about SOAP extensions and suggested using them to my colleague, he was absolutely against the idea. After deep investigation, I saw why he was. SOAP extensions, if used as described in MSDN and some other resources, really affect your application performance.
Problems with the Common Implementation
- You need to perform copying of the original stream to an in-memory stream in order to read from it multiple times.
- It happens for both incoming and outgoing messages.
- At the time you chain the stream, you don't have any information about what method of what class is currently called and can't access properties of the proxy object.
- You can't choose which extensions must be used at run-time, they are selected declaratively.
There are several ways of initializing SOAP extensions:
- Class constructor - The class constructor is called every time a SOAP extension is instantiated, and is typically used to initialize member variables.
GetInitializer
- GetInitializer
however is called just once, the first time a SOAP request to an XML Web Service's method is made. It has two overloaded versions. If a custom attribute is applied to the XML Web Service method, the first GetInitializer
method is invoked. This allows the SOAP extension to interrogate the LogicalMethodInfo
of an XML Web Service method for prototype information, or to access extension-specific data passed by a class deriving from SoapExtensionAttribute
. Unfortunately, it's not my case. The second version is called when a SOAP extension is added in the web.config. It has only one parameter - the Web Service type. Initialize
- Initialize
is called every time a SOAP request is made to an XML Web Service method, but has an advantage over the class constructor, in that the object initialized in GetInitializer
is passed to it.
None of these features were helpful to me, as I had to control the execution process depending on the run-time data.
The most terrible thing with SOAP extensions is that the only method, where you can replace the net thread with the in-memory one - ChainStream
- doesn't have any parameter except the thread, and is called before the more reasonable method ProcessMessage
. You don't even know if it is a server or client message. ProcessMessage
receives all the necessary information to make a decision, but when it is called, it's too late to change the stream. And, once you have replaced the stream in the ChainStream
method, you always have to copy it to the real stream, which affects performance and requires more memory.
How to Implement SOAP Extensions Efficiently
After some days of investigation of this problem, I managed to persuade my colleague into using SOAP extensions with some improvements in the implementation, which involve using of a special switchable stream.
Special Stream
This stream is inherited from the abstract Stream
class, and just delegates all the standard method calls to one of the two internal streams. The first is the original "old" stream, the second is a MemoryStream
that is instantiated only on demand.
Shown below is the implementation of this class:
#region TraceExtensionStream
internal class TraceExtensionStream : Stream
{
#region Fields
private Stream innerStream;
private readonly Stream originalStream;
#endregion
#region .ctor
internal TraceExtensionStream(Stream originalStream)
{
innerStream = this.originalStream = originalStream;
}
#endregion
#region New public members
public void SwitchToNewStream()
{
innerStream = new MemoryStream();
}
public void CopyOldToNew()
{
Copy(originalStream, innerStream);
innerStream.Position = 0;
}
public void CopyNewToOld()
{
Copy(innerStream, originalStream);
}
public bool IsNewStream
{
get
{
return (innerStream != originalStream);
}
}
public Stream InnerStream
{
get { return innerStream; }
}
#endregion
#region Private members
private static void Copy(Stream from, Stream to)
{
const int size = 4096;
byte[] bytes = new byte[4096];
int numBytes;
while((numBytes = from.Read(bytes, 0, size)) > 0)
to.Write(bytes, 0, numBytes);
}
#endregion
#region Overridden members
public override IAsyncResult BeginRead(byte[] buffer, int offset,
int count, AsyncCallback callback, object state)
{
return innerStream.BeginRead(buffer, offset, count, callback, state);
}
public override IAsyncResult BeginWrite(byte[] buffer, int offset,
int count, AsyncCallback callback, object state)
{
return innerStream.BeginWrite(buffer, offset, count, callback, state);
}
#endregion
}
#endregion
SOAP Extension
To create a SOAP extension, you have to implement some abstract methods, such as ChainStream
, GetIntializer
, Initialize
, and ProcessMessage
. ChainStream
will look a little simpler than in the MSDN example. It just wraps a stream in TraceExtensionStream
:
public override Stream ChainStream(Stream stream)
{
traceStream = new TraceExtensionStream(stream);
return traceStream;
}
traceStream
here is a field, where we store a reference to our stream for future use.
We have nothing to do with the following methods, so we just live them blank:
public override object GetInitializer(LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute)
{
return null;
}
public override object GetInitializer(Type WebServiceType)
{
return null;
}
public override void Initialize(object initializer)
{
}
Information is passed to the method ProcessMessage
in a parameter of type SoapMessage
. Actually, an instance of either ClientSoapMessage
or ServerSoapMessage
is passed, and we can easily check the parameter type. Here, you can separate the client messages from the server messages. As we decided before, in this example, we are interested only in the client messages.
The class ClientSoapMessage
has another interesting property - Client
. It is a link to the client proxy class derived from SoapHttpClientProtocol
. (ServerSoapMessage
in turn has a property called Server
). If we manage to extend it, we can pass any information to the Web-Service extension at run-time!
Let the clients that will support tracing implement the interface ITraceable
, declared like this:
public interface ITraceable
{
bool IsTraceRequestEnabled { get; set; }
bool IsTraceResponseEnabled { get; set; }
string ComponentName { get; set; }
}
It has the following members:
IsTraceRequestEnabled
- returns true
, if dumping of SOAP requests is on. IsTraceResponseEnabled
- returns true
, if dumping of SOAP responses is on. ComponentName
- a name of the component from which the call is performed to mark traced messages with.
Now, we declare a private method in the extension class that tries to get the ITraceable
instance from the parameter of ProcessMessage
:
private ITraceable GetTraceable(SoapMessage message)
{
SoapClientMessage clientMessage = message as SoapClientMessage;
if (clientMessage != null)
{
return clientMessage.Client as ITraceable;
}
return null;
}
Now, let's implement the ProcessMessage
itself.
It is called four times for a single web call, each at a certain stage. The stage can be read from the Stage
property of the SoapMessage
, and it may have one of the four values:
BeforeSerialize
- occurs before the client request (or server response) is serialized. Here, we can prepare our smart stream for buffering, if needed. AfterSerialize
- occurs after the client request (or server response) is serialized. Now, we can write the buffer to the log. BeforeDeserialize
- occurs before the client response (or server request) is deserialized. Here, we can copy the response to the buffer and save it to the log. After that, we must make the buffer active and reset its position. AfterDeserialize
- occurs after the client response (or server request) is deserialized. We won't do anything at this stage.
Here is the implementation:
public override void ProcessMessage(SoapMessage message)
{
ITraceable traceable = GetTraceable(message);
if (traceable == null) return;
switch (message.Stage)
{
case SoapMessageStage.BeforeSerialize:
if (traceable.IsTraceRequestEnabled)
{
traceStream.SwitchToNewStream();
}
break;
case SoapMessageStage.AfterSerialize:
if (traceStream.IsNewStream)
{
traceStream.Position = 0;
WriteToLog(DumpType.Request, traceable);
traceStream.Position = 0;
traceStream.CopyNewToOld();
}
break;
case SoapMessageStage.BeforeDeserialize:
if (traceable.IsTraceResponseEnabled)
{
traceStream.SwitchToNewStream();
traceStream.CopyOldToNew();
WriteToLog(DumpType.Response, traceable);
traceStream.Position = 0;
}
break;
}
}
That's it. Now, you only need to make your client protocol to support ITraceable
.
Extending SoapHttpClientProtocol
If you implemented your client proxy class (SoapHttpClientProtocol
) manually, it is not a problem to add an additional interface to support. But, if it was generated automatically, you probably don't want to modify the auto-generated file. Lickily, in that file, it's declared as partial
. It means that the proxy class can be extended in another file.
public partial class MyService : ITraceable
{
private string componentName;
private bool isTraceRequestEnabled;
private bool isTraceResponseEnabled;
public bool IsTraceRequestEnabled
{
get { return isTraceRequestEnabled; }
set { isTraceRequestEnabled = value; }
}
public bool IsTraceResponseEnabled
{
get { return isTraceResponseEnabled; }
set { isTraceResponseEnabled = value; }
}
public string ComponentName
{
get { return componentName; }
set { componentName = value; }
}
}
Now, you only need to set the values of these properties, and you can control the use of your SOAP extension without permanently affecting performance.
How to Use the Code
Obvously, you need an application with a reference to a web-service. It can be a web application, Windows application, or another web-service.
First, copy the enclosed code file into your project. You may want to change the namespace and the tracing method.
Then you should extend your web-service client proxy class as described above. If there is a logic defining whether logging should occur or not, it will go there.
Finally, make sure to include a reference to your SOAP extension in the web.config (or app.config) file. You should get something similar to this.
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add type="Ideafixxxer.SoapDumper.TraceExtension" priority="1" group="0"/>
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
I hope this article helps somebody. Any comments and questions are welcome.