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
{
public override object GetInitializer(
LogicalMethodInfo methodInfo,
SoapExtensionAttribute attribute )
{
return null;
}
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 )
{
header as SoapSizeHeader;
_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