Click here to Skip to main content
Click here to Skip to main content

Efficient Tracing Using SOAP Extensions in .NET

By , 22 Mar 2009
 

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 a deep investigation, I saw why he was. SOAP extensions, if used as written in MSDN and some other resources, really affect 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 when you chain a 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 is made to an XML Web Service's method. 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.

Having to control the execution process relying on run-time data, I had nothing to do with these features.

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's a server or client message. ProcessMessage receives all the necessary information to make a decision, but when it's called, it's too late to change the stream. And, once you have replaced the stream in ChainStream, 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

/// <summary>
/// Special switchable stream
/// </summary>
internal class TraceExtensionStream : Stream
{
    #region Fields

    private Stream innerStream;
    private readonly Stream originalStream;

    #endregion

    #region .ctor

    /// <summary>
    /// Constructs an instance of the stream
    /// wrapping the original stream into it
    /// </summary>
    internal TraceExtensionStream(Stream originalStream)
    {
        innerStream = this.originalStream = originalStream;
    }

    #endregion

    #region New public members

    /// <summary>
    /// Creates a new memory stream and makes it active
    /// </summary>
    public void SwitchToNewStream()
    {
        innerStream = new MemoryStream();
    }

    /// <summary>
    /// Copies data from the old stream to the new in-memory stream
    /// </summary>
    public void CopyOldToNew()
    {
        //innerStream = new MemoryStream((int)originalStream.Length);
        Copy(originalStream, innerStream);
        innerStream.Position = 0;
    }

    /// <summary>
    /// Copies data from the new stream to the old stream
    /// </summary>
    public void CopyNewToOld()
    {
        Copy(innerStream, originalStream);
    }

    /// <summary>
    /// Returns true if the active inner stream is a new stream,
    /// i.e. SwitchToNewStream has been called
    /// 
    public bool IsNewStream
    {
        get
        {
            return (innerStream != originalStream);
        }
    }

    /// <summary>
    /// A link to the active inner stream
    /// </summary>
    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);
    }

    //other overriden abstract members of Stream go down here

    #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:

/// <summary>
/// Replaces soap stream with our smart stream
/// </summary>
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 Server). If we manage to extend it, we can pass any information to the Web-Service at run-time!

Let the clients that support dumping implement the interface ITraceable, declared like this:

/// <summary>
/// Interface that a proxy class should implement to support tracing
/// </summary>
public interface ITraceable
{
    bool IsTraceRequestEnabled { get; set; }
    bool IsTraceResponseEnabled { get; set; }
    string ComponentName { get; set; }
}

It has the following members:

  • IsTraceRequestEnabled - returns true, if dump of SOAP requests is on.
  • IsTraceResponseEnabled - returns true, if dump 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:

/// <summary>
/// Tries to get ITraceable instance
/// </summary>
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 has 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 proxy is not configured to be traced, return
    if (traceable == null) return;
    switch (message.Stage)
    {
        case SoapMessageStage.BeforeSerialize:
            //If tracing is enabled, switch to memory buffer
            if (traceable.IsTraceRequestEnabled)
            {
                traceStream.SwitchToNewStream();
            }
            break;
        case SoapMessageStage.AfterSerialize:
            //If message was written to memory buffer, 
            //write its content to log and copy to the SOAP stream
            if (traceStream.IsNewStream)
            {
                traceStream.Position = 0;
                WriteToLog(DumpType.Request, traceable);
                traceStream.Position = 0;
                traceStream.CopyNewToOld();
            }
            break;
        case SoapMessageStage.BeforeDeserialize:
            //If tracing is enabled, copy SOAP stream 
            //to the new stream and write its content to log
            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's not a problem to add an additional interface to support. But, if it was generated automatically, you probably wouldn't want to modify the auto-generated file. Hopefully, 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.

I hope this article helps somebody. Any comments and questions are welcome. I also would love know how this problem is resolved in .NET Framework 3.5 with WCF.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

ideafixxxer
Software Developer (Senior) EPAM Systems
Russian Federation Russian Federation
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionHow to implement this??memberTridip Bhattacharjee19-Nov-12 3:08 
AnswerRe: How to implement this??memberideafixxxer19-Nov-12 7:52 
GeneralRe: How to implement this??memberTridip Bhattacharjee19-Nov-12 19:52 
GeneralI downloaded code that you have provided but no logfile is createdmemberGatewayNeha113-Jun-11 0:40 
AnswerRe: I downloaded code that you have provided but no logfile is createdmemberborisco18-Jun-11 3:07 
QuestionNot WorkingmemberMember 76961701-Mar-11 6:58 
GeneralDisposing of a memory stream [modified]memberkkgb12-Apr-10 1:38 
AnswerRe: Disposing of a memory streammemberideafixxxer12-Apr-10 3:49 
GeneralAdd SoapExtensionattributememberMathewUthup3-Oct-09 17:42 
GeneralRe: Add SoapExtensionattributememberideafixxxer4-Oct-09 6:29 
GeneralDumpTypememberpavlo200927-Apr-09 1:33 
AnswerRe: DumpTypememberideafixxxer27-Apr-09 2:51 
QuestionI do not understand the difference between the old and the new way of implementationmemberSivajish25-Mar-09 5:53 
AnswerRe: I do not understand the difference between the old and the new way of implementationmemberideafixxxer25-Mar-09 6:29 
GeneralRe: I do not understand the difference between the old and the new way of implementationmemberSivajish25-Mar-09 9:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130619.1 | Last Updated 22 Mar 2009
Article Copyright 2009 by ideafixxxer
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid