Find the size of the SOAP messages in your web service

A simple SoapExtension that examines the SOAP messages being passed to and from your web service.
By Nicholas Butler

profile for Nicholas Butler at Stack Overflow, Q&A for professional and enthusiast programmers
Publicly recommend on Google


Introduction

This is a short, simple article that introduces SOAP headers and SOAP extensions.

The code intercepts the SOAP messages as they pass through your web service, and makes their size available to your service and client as a SOAP header. The code can also save the messages to XML files on your web server, as it is often useful to see the actual SOAP messages that are being transferred while you are writing a web service.

It's not rocket science, but I have found it very useful.

Using the code

As you probably know, each SOAP envelope contains two parts: the headers and the body. The aim of this code is to populate a SOAP header with the size of the SOAP message, so that it is accessible from within your web service. This is done using a SoapExtension.

This extension also saves the SOAP messages to XML files on your web server for debugging purposes, but this functionality is only enabled in DEBUG builds. It saves the input and output SOAP to files in the "Soap" subdirectory of your web application folder, so if you use this feature, you must make sure that this directory exists and that the ASPNET user has write access to it.

SOAP header

Firstly, we need a header class to hold the size information. Declaring a new SOAP header is simple:

public class SoapSizeHeader : SoapHeader
{
    private int _Size = 0;

    public int SoapSizeHeaderSize
    {
        get { return _Size; }
        set { _Size = value; }
    }
}

Next, we need to add this header to your WebService. You can either derive your WebService from the following class, or just add the field and property directly:

public class SoapSizeService : WebService
{
    private SoapSizeHeader _SoapSizeHeader = new SoapSizeHeader();

    public SoapSizeHeader SoapSizeHeader
    {
        get { return _SoapSizeHeader; }
        set { _SoapSizeHeader = value; }
    }
}

Attributes

The main code will be in a class called SoapSizeExtension, which derives from SoapExtension. Before I get to this, I will show you how to hook the new header and extension from your WebService class.

To add your header to the SOAP that is passed, you add a SoapHeaderAttribute to your web method in your WebService class. You also use an attribute to add the SOAP extension:

partial class MyWebService : SoapSizeService
{
    [WebMethod]
    [SoapHeader( "SoapSizeHeader", Direction = SoapHeaderDirection.InOut )]
    [SoapSizeExtension]
    public string HelloWorld()
    {
        return "Hello, world";
    }
}

The SoapSizeExtensionAttribute class is quite simple - it just overrides the ExtensionType property to return the type of the SoapExtension:

[AttributeUsage( AttributeTargets.Method )]
public class SoapSizeExtensionAttribute : SoapExtensionAttribute 
{
    private int _Priority;

    public override Type ExtensionType 
    {
        get { return typeof( SoapSizeExtension ); }
    }
    
    public override int Priority 
    {
        get { return _Priority; }
        set { _Priority = value; }
    }
}

SoapSizeExtension

This class does the work. It hooks into the message processing system to gain access to the raw SOAP that is being passed. This is quite an involved process, so I shall explain the class step by step.

Firstly, we declare the class and add some private fields:

public partial class SoapSizeExtension : SoapExtension
{
    private Stream _OldStream = null;
    private Stream _NewStream = null;
    private string _SoapInput = null;

#if DEBUG

    private string _InputFilepath = null;
    private string _OuputFilepath = null;

#endif

}

Initialization: The SoapExtension class has some abstract members used for initialization. We are not using these, so we just stub them out:

partial class SoapSizeExtension
{
    // The SOAP extension was configured to run using an attribute
    public override object GetInitializer(
            LogicalMethodInfo methodInfo,
            SoapExtensionAttribute attribute )
    {
        return null;
    }

    // The SOAP extension was configured to run using a configuration file
    public override object GetInitializer(Type serviceType)
    {
        return null;
    }

    public override void Initialize( object initializer )
    {
    }
}

ChainStream: This is the first interesting method. It gives us a chance to separate the input and the output streams in the message processing cycle. Here, we save a reference to the input stream, and return a new stream that we will fill later:

partial class SoapSizeExtension
{
    public override Stream ChainStream( Stream stream )
    {
        _OldStream = stream;

        return _NewStream = new MemoryStream();
    }
}

ProcessMessage: This is the most important method. It is called four times during each cycle, each time with a different SoapMessageStage. Firstly, when a message arrives from a client, it is deserialized from the SOAP and BeforeDeserialize and AfterDeserialize are called. Then the service does its work and the result is serialized to SOAP and BeforeSerialize and AfterSerialize are called. This method just calls a private method at each stage:

partial class SoapSizeExtension
{
    public override void ProcessMessage( SoapMessage message )
    {
        switch ( message.Stage ) 
        {
        case SoapMessageStage.BeforeDeserialize:
            BeforeDeserialize();
            break;

        case SoapMessageStage.AfterDeserialize:
            AfterDeserialize( message );
            break;

        case SoapMessageStage.BeforeSerialize:
            BeforeSerialize( message );
            break;

        case SoapMessageStage.AfterSerialize:
            AfterSerialize();
            break;

        default: throw new InvalidOperationException("Invalid stage");
        }
    }
}

SOAP header: Before we look at the individual methods, we need to know what we are trying to do. This is the SOAP that we are trying to alter:

<soap:Header>
    <SoapSizeHeader xmlns="http://tempuri.org">
        <SoapSizeHeaderSize>1764</SoapSizeHeaderSize>
    </SoapSizeHeader>
</soap:Header>

Regex: We need to find the SoapSizeHeaderSize element and set its value to the size of the SOAP message. This will make the value available to the web service. This is most easily achieved using a regular expression:

partial class SoapSizeExtension
{
    private static Regex regexSoapSizeHeaderSize = new Regex(
           @"(?<before><soap:Header>.*?<SoapSizeHeaderSize>)" +
           @"\d+" +
           @"(?<after></SoapSizeHeaderSize>.*?</soap:Header>)",
           RegexOptions.IgnoreCase |
           RegexOptions.Singleline |
           RegexOptions.Compiled
           );
}

BeforeDeserialize: Now we can look at the individual methods. The BeforeDeserialize method is the first to be called. In this method, we have access to the incoming SOAP message from the client. We read the message from the _OldStream, get its length and set the header value, and then write the modified message to the _NewStream:

partial class SoapSizeExtension
{
    private void BeforeDeserialize()
    {
        StreamReader input = new StreamReader( _OldStream );
        _SoapInput = input.ReadToEnd();
        _OldStream.Position = 0;

        int length = _SoapInput.Length;
        _SoapInput = regexSoapSizeHeaderSize.Replace(
            _SoapInput, "${before}" + length + "${after}", 1 );

        StreamWriter output = new StreamWriter( _NewStream );
        output.Write( _SoapInput );
        output.Flush();
        _NewStream.Position = 0;
    }
}

AfterDeserialize: When the _NewStream has been deserialized by the system, AfterDeserialize is called. In this method, we have access to our WebService object, so we can use the MapPath method to find the application directory on the web server. We can then write the modified SOAP message to a file. The commented code is an alternative way of setting the SoapSizeHeader value:

partial class SoapSizeExtension
{
    private void AfterDeserialize( SoapMessage message )
    {
        //foreach ( SoapHeader header in message.Headers )
        //{
        //    SoapSizeHeader soapSizeHeader = header as SoapSizeHeader;                           
        //
        //    if ( soapSizeHeader != null )
        //        soapSizeHeader.SoapSizeHeaderSize =  _SoapInput.Length;
        //}

#if DEBUG

        if ( _InputFilepath == null )
        {
            SoapServerMessage serverMessage = message as SoapServerMessage;
            WebService service = serverMessage.Server as WebService;
            _InputFilepath = service.Server.MapPath( "Soap\\input.xml" );
        }

        WriteFile( _InputFilepath, _SoapInput );

#endif

        _SoapInput = null;
    }
}

BeforeSerialize: After the web method has executed, the return values are serialized to SOAP to return to the client. BeforeSerialize is called first, and here we just use the WebService object to resolve a path for us again:

partial class SoapSizeExtension
{
    private void BeforeSerialize( SoapMessage message )
    {

#if DEBUG

        if ( _OuputFilepath == null )
        {
            SoapServerMessage serverMessage = message as SoapServerMessage;
            WebService service = serverMessage.Server as WebService;
            _OuputFilepath = service.Server.MapPath( "Soap\\output.xml" );
        }

#endif
    
    }
}

AfterSerialize: This is the last method. Here we have access to the response SOAP, so we can set the SoapSizeHeader value for the client:

partial class SoapSizeExtension
{
    private void AfterSerialize()
    {
        _NewStream.Position = 0;
        StreamReader input = new StreamReader( _NewStream );
        string soap = input.ReadToEnd();

        int length = soap.Length;
        soap = regexSoapSizeHeaderSize.Replace(
            soap, "${before}" + length + "${after}", 1 );

        StreamWriter output = new StreamWriter( _OldStream );
        output.Write( soap );
        output.Flush();

#if DEBUG

        WriteFile( _OuputFilepath, soap );

#endif

    }
}

WriteFile: The last private method is just a helper method to store the SOAP messages to the files:

partial class SoapSizeExtension
{

#if DEBUG

    private void WriteFile( string filepath, string soap )
    {
        FileStream fs = null;
        StreamWriter writer = null;
        try
        {
            fs = new FileStream( filepath, FileMode.Create, FileAccess.Write );
            writer = new StreamWriter( fs );
            writer.Write( soap );
        }
        finally
        {
            if ( writer != null ) writer.Close();
            if ( fs != null ) fs.Close();
        }
    }
#endif
}

WebMethod

With the SoapSizeExtension in place, we can now access the SoapSizeHeader value from our WebMethod:

partial class MyWebService : SoapSizeService
{
    [WebMethod]
    [SoapHeader( "SoapSizeHeader", Direction=SoapHeaderDirection.InOut )]
    [SoapSizeExtension]
    public string HelloWorld()
    {
        if (this.SoapSizeHeader.SoapSizeHeaderSize > 1024)
            return "You talk too much";

        return "Hello, world";
    }
}

Conclusion

This code is a very simple use of a SoapHeader and a SoapExtension. You can use it as it is, but you can also use it as a base for writing more complex extensions.

History

  • 14th December, 2005: Version 1
    • First release.

License

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

Contact

If you have any feedback, please feel free to contact me.

Publicly recommend on Google: