Click here to Skip to main content
15,867,686 members
Articles / Web Development / ASP.NET

Efficient Tracing Using SOAP Extensions in .NET

Rate me:
Please Sign up or sign in to vote.
4.74/5 (26 votes)
11 Feb 2015CPOL6 min read 155.2K   4K   42   51
Describes how to avoid loss of performance when implementing SOAP extensions.

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:

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

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

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

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

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

C#
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 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.

C#
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.

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

License

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


Written By
Team Leader
Canada Canada
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 4 Pin
Jan Hussaarts8-Oct-21 9:12
Jan Hussaarts8-Oct-21 9:12 
QuestionHow can I use this in a web-service? Pin
Jan Hussaarts6-Oct-21 9:50
Jan Hussaarts6-Oct-21 9:50 
AnswerRe: How can I use this in a web-service? Pin
ideafixxxer6-Oct-21 15:47
ideafixxxer6-Oct-21 15:47 
GeneralRe: How can I use this in a web-service? Pin
Jan Hussaarts6-Oct-21 21:23
Jan Hussaarts6-Oct-21 21:23 
GeneralRe: How can I use this in a web-service? Pin
Jan Hussaarts7-Oct-21 23:22
Jan Hussaarts7-Oct-21 23:22 
GeneralRe: How can I use this in a web-service? Pin
ideafixxxer8-Oct-21 4:15
ideafixxxer8-Oct-21 4:15 
GeneralRe: How can I use this in a web-service? Pin
Jan Hussaarts8-Oct-21 9:09
Jan Hussaarts8-Oct-21 9:09 
QuestionUnit test on the given Trace Extention class Pin
MayurPadhiyar20-Nov-19 21:51
MayurPadhiyar20-Nov-19 21:51 
QuestionMy Vote of 5 Pin
R2B219-Apr-19 4:25
R2B219-Apr-19 4:25 
QuestionWCF Services Pin
kiquenet.com24-Jun-18 21:44
professionalkiquenet.com24-Jun-18 21:44 
PraiseVote 5 Pin
tsoh_tan27-Mar-18 22:22
tsoh_tan27-Mar-18 22:22 
QuestionResponse is not well-formed XML. Pin
Lechuss10-Jan-18 4:12
Lechuss10-Jan-18 4:12 
QuestionApply SOAPExtension to particular WebReference Pin
siddharth_jain21028-Apr-17 3:33
siddharth_jain21028-Apr-17 3:33 
i have a project in which in have more than one WebReference added. Out of all WebReferences i want to apply the SOAP Extension to just one WebReference.
Adding the section in the config will apply the Extension to all of the WebReferences in the project. Is there any way, i can configure this for one WebReference only?
AnswerRe: Apply SOAPExtension to particular WebReference Pin
ideafixxxer20-Sep-17 5:06
ideafixxxer20-Sep-17 5:06 
GeneralMy Vote of 5 Pin
R2B230-Mar-17 11:19
R2B230-Mar-17 11:19 
QuestionWriteToLog() *1.51 factor for StringWriter/StringBuilder? Pin
pikarde27-Feb-17 10:07
pikarde27-Feb-17 10:07 
AnswerRe: WriteToLog() *1.51 factor for StringWriter/StringBuilder? Pin
R2B230-Mar-17 10:53
R2B230-Mar-17 10:53 
GeneralRe: WriteToLog() *1.51 factor for StringWriter/StringBuilder? Pin
pikarde2-Jul-17 19:45
pikarde2-Jul-17 19:45 
QuestionSOAP Header Pin
Member 126879256-Sep-16 22:13
Member 126879256-Sep-16 22:13 
AnswerRe: SOAP Header Pin
ideafixxxer20-Sep-17 5:07
ideafixxxer20-Sep-17 5:07 
QuestionSample application Pin
sheir21-Mar-16 5:13
sheir21-Mar-16 5:13 
AnswerRe: Sample application Pin
ideafixxxer23-Mar-16 4:48
ideafixxxer23-Mar-16 4:48 
QuestionIncorect group value in web.config Pin
Lechuss8-Mar-16 5:55
Lechuss8-Mar-16 5:55 
AnswerRe: Incorect group value in web.config Pin
ideafixxxer23-Mar-16 4:53
ideafixxxer23-Mar-16 4:53 
QuestionMake relation between a pair of request & response Pin
yogiCsharp5-Jun-15 22:50
yogiCsharp5-Jun-15 22:50 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.