Altering the SOAP Message Using SOAP Extensions

SOAP extensions allow developers to augment the functionality of an XML Web service by altering the SOAP messages sent to and from an XML Web service or an XML Web service client. For instance, you can implement an encryption or compression algorithm to run with an existing XML Web service.

To understand how a SOAP extension works, it is useful to first understand the lifetime of an XML Web service. For an overview of the lifetime of an XML Web service, see Anatomy of an XML Web Service Lifetime. However, the following graphic outlines the major phases of a call from a client to an XML Web service.

Anatomy of an XML Web service lifetime

esw638yk.xmlwebservicelifetime(en-us,VS.71).gif

As you can see, ASP.NET serializes and deserializes XML during phases on both the XML Web service computer and the XML Web service client computer. A SOAP extension can be injected into the infrastructure to inspect or modify the SOAP messages before and after each of these serialize and deserialize phases. For instance, an encryption SOAP extension might encrypt the XML portion of the SOAP message after ASP.NET serializes the client's arguments, and then decrypt the SOAP message on the Web server before ASP.NET deserializes the SOAP message. These phases, where a SOAP extension might inspect or modify the SOAP message, are defined in the SoapMessageStage enumeration. In this case, the SOAP extension is encrypting in the AfterSerialize stage and decrypting in the BeforeDeserialize stage.

Typically, when a SOAP extension modifies the contents of a SOAP message, the modifications must be done on both the client and the server. That is, if a SOAP extension were to run on the client and encrypt the SOAP message, a corresponding SOAP extension must decrypt the SOAP message on the server. If the SOAP message is not decrypted, then the ASP.NET infrastructure cannot deserialize the SOAP message into an object. Of course, a SOAP extension that does not modify the SOAP message, such as a SOAP extension that simply logs the SOAP messages, can run on only the client or server. In this case, the recipient receives the same SOAP message it would have if a SOAP extension were not running and the ASP.NET infrastructure can deserialize the SOAP message. Also, if a the SOAP extension does not modify the SOAP in a way that makes deserialization impossible, the SOAP extension does not need to run on both the client and server.

Now that you know what a SOAP extension can do, and when it can perform its functionality, let's look at how to build a SOAP extension. The basic steps to build a SOAP extension and have it run with an XML Web service are the following:

  • Derive a class from SoapExtension.
  • Save a reference to the Stream representing future SOAP messages.
  • Initialize SOAP extension specific data.
  • Process the SOAP messages during the pertinent SoapMessageStage or stages.
  • Configure the SOAP extension to run with specific XML Web service methods.

Derive a Class from SoapExtension

The class deriving from SoapExtension is the class that performs the functionality of the SOAP extension. That is, if the SOAP extension is an encryption SOAP extension, then the class deriving from the SoapExtension class performs the encryption and corresponding decryption.

Save a Reference to the Stream Representing Future SOAP Messages

To modify a SOAP message, you must override the ChainStream method, as that is the only opportunity to receive a reference to the stream that can be used to obtain the contents of future SOAP messages.

A reference to a Stream is passed into the ChainStream once, prior to any SoapMessageStage. This Stream refers to the XML of the SOAP message after SOAP extensions of lower priority (see Configure the SOAP Extension to Run with XML Web Service Methods for details about SOAP extension priorities) have executed and made their changes to the SOAP message. Therefore, a SOAP extension should save this reference in a member variable for later access during the SoapMessageStage, when a SOAP extension inspects or modifies the SOAP message.

The Stream passed into ChainStream, however, is not the Stream a SOAP extension should modify. A SOAP extension should create a new instance of a Stream, save that in a private member variable, and return it within the ChainStream method. As the SOAP extension executes during each SoapMessageStage and modifies the SOAP message, a SOAP extension should read from the Stream passed into ChainStream and write to the Stream return value for ChainStream. Therefore, it is important to save both Stream references within the ChainStream method.

The following example demonstrates a common implementation of the ChainStream method.

    ' Save the Stream representing the SOAP request or SOAP response
    ' into a local memory buffer.
    Public Overrides Function ChainStream(stream As Stream) As Stream
        ' Save the passed in Stream in a member variable.
        oldStream = stream

        ' Create a new instance of a Stream and save that in a member
        ' variable.
        newStream = New MemoryStream()
        Return newStream
    End Function
[C#]
    // Save the Stream representing the SOAP request or SOAP response into
    // a local memory buffer.
    public override Stream ChainStream( Stream stream ){
        // Save the passed in Stream in a member variable.
        oldStream = stream;

        // Create a new instance of a Stream and save that in a member
        // variable.
        newStream = new MemoryStream();
        return newStream;
    }

Initialize SOAP Extension Specific Data

A SOAP extension can initialize internal data, based on the XML Web service or XML Web service method it is applied to. For instance, a SOAP extension that logs the SOAP message sent to and from an XML Web service method might initialize the name of a file to save the logging information (based on the name of the XML Web service or the XML Web service method the SOAP extension is running with).

The class deriving from SoapExtension has two methods for initializing data: GetInitializer and Initialize. GetInitializer is called only the first time an XML Web service or XML Web service method is accessed that the SOAP extension is configured to run with, depending on how the SOAP extension is configured (see Configure the SOAP Extension to Run with XML Web Service Methods). If the SOAP extension is configured using an attribute, GetInitializer is called by the ASP.NET infrastructure on a per-XML Web service method basis. If the SOAP extension is configured in a configuration file, GetInitializer is called by the ASP.NET infrastructure only the first time an XML Web service is accessed. The data returned from GetInitializer by a SOAP extension is cached by the ASP.NET infrastructure for future use by the SOAP extensions. SOAP extensions are passed the cached data each time the SOAP extension runs with that XML Web service or XML Web service method in the Initialize method.

The information available to a SOAP extension in the GetInitializer method depends upon how the SOAP extension is configured. If the SOAP extension is configured using an attribute, the attribute, including any custom properties associated with it, and a LogicalMethodInfo are passed into GetInitializer by the ASP.NET infrastructure. The LogicalMethodInfo provides prototype details about the XML Web service method, such as the number of parameters and their data types. If the SOAP extension is configured using a configuration file, just the Type of the class implementing the XML Web service is passed to GetInitializer.

The following code example initializes the cached piece of data in the GetInitializer method differently, based on how the SOAP extension is eventually configured. If the SOAP extension is configured using an attribute, in this case a TraceExtensionAttribute, the file name specified in the attribute is cached. If the SOAP extension is configured using a configuration file, the cached file name is computed, based on the type of the XML Web service.

' When the SOAP extension is accessed for the first time, the XML
' Web service method it is applied to is accessed to store the file
' name passed in, using the corresponding SoapExtensionAttribute.
Public Overloads Overrides Function GetInitializer(methodInfo As _
   LogicalMethodInfo, attribute As SoapExtensionAttribute) As Object 
 Return CType(attribute, TraceExtensionAttribute).Filename
End Function

' The extension was configured to run using a configuration file 
' instead of an attribute applied to a specific XML Web service method.
' Return a file name, based on the class implementing the XML Web 
' service's type.

Public Overloads Overrides Function GetInitializer(WebServiceType As _
   Type) As Object
  ' Return a file name to log the trace information, based on the type.
  Return "C:\" + WebServiceType.FullName + ".log"    
End Function
[C#]
// When the SOAP extension is accessed for the first time, the XML
// Web service method it is applied to is accessed to store the file
// name passed in, using the corresponding SoapExtensionAttribute.
public override object GetInitializer(LogicalMethodInfo methodInfo,
   SoapExtensionAttribute attribute) 
{
   return ((TraceExtensionAttribute) attribute).Filename;
}
// The extension was configured to run using a configuration file instead of
// an attribute applied to a specific XML Web service method.
public override object GetInitializer(Type WebServiceType) 
{
// Return a file name to log the trace information, based on the type.
   return "C:\\" + WebServiceType.FullName + ".log";}

Process the SOAP Messages

In the class derived from SoapExtension, the core piece of implementation is the SoapExtension.ProcessMessage method. This method is called several times by ASP.NET at each of the stages defined in the SoapMessageStage enumeration. Each time SoapExtension.ProcessMessage method is called, a SoapMessage, or a class deriving from it, is passed in with information about the SOAP message at that particular stage. If the SOAP extension is running with an XML Web service, then a SoapServerMessage is passed in. If the SOAP extension is running with an XML Web service client, then a SoapClientMessage is passed in.

The following code example is the ProcessStage method of a SOAP extension that traces a call to an XML Web service. During the trace, if the SoapMessageStage is one where the parameters have been serialized into XML, the XML is written to a file.

    public override void ProcessMessage(SoapMessage message) 
    {
        switch (message.Stage) 
        {
        case SoapMessageStage.BeforeSerialize:
            break;
        case SoapMessageStage.AfterSerialize:
// Write the SOAP message out to a file.
            WriteOutput( message );
            break;
        case SoapMessageStage.BeforeDeserialize:
//Write the SOAP message out to a file.
            WriteInput( message );
            break;
        case SoapMessageStage.AfterDeserialize:
            break;
        default:
            throw new Exception("invalid stage");
        }
    }
[Visual Basic]
    Public Overrides Sub ProcessMessage(message As SoapMessage)
        Select Case message.Stage
           Case SoapMessageStage.BeforeSerialize
Case SoapMessageStage.AfterSerialize                ' Write the SOAP message out to a file.
                WriteOutput(message)Case SoapMessageStage.BeforeDeserialize' Write the SOAP messae out to a file.
                WriteInput(message)
           Case SoapMessageStage.AfterDeserialize
           Case Else
                Throw New Exception("invalid stage")
        End Select
    End Sub

Order SOAP Extensions Methods are Invoked

Now that you have looked at the methods a SOAP extension must override, let's look at when ASP.NET invokes SOAP extension methods throughout the invocation of an XML Web service method. The following steps assume that the SOAP extension is running on both the client and server. If the SOAP extension is not running on both the client and the server, the steps associated with the SOAP extension running on each are ignored by ASP.NET.

Client side

  1. A client invokes a method on the proxy class.
  2. A new instance of the SOAP extension is created on the client.
  3. If this is the first time this SOAP extension has executed with this XML Web service on the client, then the GetInitializer method is invoked on the SOAP extension running on the client.
  4. The Initialize method is invoked.
  5. The ChainStream method is invoked.
  6. The ProcessMessage method is invoked with the SoapMessageStage set to BeforeSerialize.
  7. ASP.NET on the client computer serializes the arguments of the XML Web service method into XML.
  8. The ProcessMessage method is invoked with the SoapMessageStage set to AfterSerialize.
  9. ASP.NET on the client computer sends the SOAP message over the network to the Web server hosting the XML Web service.

Server side

  1. ASP.NET on the Web server receives the SOAP message.
  2. A new instance of the SOAP extension is created on the Web server.
  3. On the Web server, if this is the first time this SOAP extension has executed with this XML Web service on the server side, the GetInitializer method is invoked on the SOAP extension running on the server.
  4. The Initialize method is invoked.
  5. The ChainStream method is invoked.
  6. The ProcessMessage method is invoked with the SoapMessageStage set to BeforeDeserialize.
  7. ASP.NET deserializes the arguments within the XML.
  8. The ProcessMessage method is invoked with the SoapMessageStage set to AfterDeserialize.
  9. ASP.NET creates a new instance of the class implementing the XML Web service and invokes the XML Web service method, passing in the deserialized arguments. This object resides on the same computer as the Web server.
  10. The XML Web service method executes its code, eventually setting the return value and any out parameters.
  11. The ProcessMessage method is invoked with the SoapMessageStage set to BeforeSerialize.
  12. ASP.NET on the Web server serializes the return value and out parameters into XML.
  13. The ProcessMessage method is invoked with the SoapMessageStage set to AfterSerialize.
  14. ASP.NET sends the SOAP response message over the network back to the XML Web service client.

Client side

  1. ASP.NET on the client computer receives the SOAP message.
  2. The ProcessMessage method is invoked with the SoapMessageStage set to BeforeDeserialize.
  3. ASP.NET deserializes the XML into the return value and any out parameters.
  4. The ProcessMessage method is invoked with the SoapMessageStage set to AfterDeserialize.
  5. ASP.NET passes the return value and any out parameters to the instance of the proxy class.
  6. The client receives the return value and any out parameters.

Configure the SOAP Extension to Run with XML Web Service Methods

A SOAP extension can be configured to run using a custom attribute or configured to run by modifying a configuration file. To use a custom attribute, apply it to each XML Web service method you want the SOAP extension to run with. When a configuration file is used, the SOAP extension runs with all XML Web services within the scope of the configuration file. For details on how configuration files work, see Configuring Applications.

To use a custom attribute, derive a class from SoapExtensionAttribute. SoapExtensionAttribute has two properties: ExtensionType and Priority. A SOAP extension should return the type of the SOAP extension in the ExtensionType property. The Priority property represents the relative priority of the SOAP extension, which is discussed shortly.

To specify that a SOAP extension runs with all XML Web services within the scope of a configuration file, add entries to either the appropriate App.config or Web.config file. Specifically, a soapExtensionTypes XML element must be added to the webServices section of the configuration file. Within the soapExtensionTypes XML element, add XML elements for each SOAP extension you want to run with every XML Web service within the scope of the configuration file. The add XML element has the following properties.

Property Description
type The type of the SOAP extension and the assembly it resides in.
priority The relative priority of the SOAP extension within its group.
group The group a SOAP extension is a member of. See the details following this table on priority.

SOAP extensions have a priority assigned to them that dictates the relative order of execution when multiple SOAP extensions are configured to run with an XML Web service method. The higher the priority a SOAP extension is, the closer it executes to the SOAP message being sent or received over the network. SOAP extensions are in one of three priority groups. Within each group, the priority property distinguishes each member. The lower the priority property is, the higher the relative priority (0 being the highest).

The three relative priority groups for SOAP extensions are: SOAP extensions configured using an attribute and SOAP extensions specified in the configuration file with a group setting of 0 or 1. SOAP extensions configured using an attribute are members of the medium group. SOAP extensions configured using a configuration file with a group setting of 0 have the highest relative priority. Those with a group setting of 1 have the lowest relative priority.

The following code example is a configuration file that specifies that the Logger.LoggerExtension SOAP extension runs within the relative priority group 0 and has a priority of 1.

<configuration>
 <system.web>
   <webServices>
     <soapExtensionTypes>      <add type="Logger.LoggerExtension,logger"           priority="1"           group="0" />     </soapExtensionTypes>
    </webServices>
 </system.web>
</configuration>

The following code example is a SOAP extension that logs the SOAP messages sent to and from an XML Web service or XML Web service client. If the following SOAP extension is installed to run with an XML Web service, the ASPNET user account must have permission to write to the directory in which the log file is written.

Imports System
Imports System.Web.Services
Imports System.Web.Services.Protocols
Imports System.IO

' Define a SOAP Extension that traces the SOAP request and SOAP response
' for the XML Web service method the SOAP extension is applied to.
Public Class TraceExtension
    Inherits SoapExtension
    
    Private oldStream As Stream
    Private newStream As Stream
    Private m_filename As String    
    
    ' Save the Stream representing the SOAP request or SOAP response into
    ' a local memory buffer.
    Public Overrides Function ChainStream(stream As Stream) As Stream
        oldStream = stream
        newStream = New MemoryStream()
        Return newStream
    End Function

    ' When the SOAP extension is accessed for the first time, the XML Web
    ' service method it is applied to is accessed to store the file
    ' name passed in, using the corresponding SoapExtensionAttribute.
    Public Overloads Overrides Function GetInitializer(methodInfo As _ 
        LogicalMethodInfo, _
      attribute As SoapExtensionAttribute) As Object 
      Return CType(attribute, TraceExtensionAttribute).Filename
    End Function

    ' The SOAP extension was configured to run using a configuration file
    ' instead of an attribute applied to a specific XML Web service
    ' method.  Return a file name based on the class implementing the Web
    ' Service's type.
    Public Overloads Overrides Function GetInitializer(WebServiceType As _
      Type) As Object
      ' Return a file name to log the trace information to, based on the
      ' type.
      Return "C:\" + WebServiceType.FullName + ".log"    
    End Function

    ' Receive the file name stored by GetInitializer and store it in a
    ' member variable for this specific instance.
    Public Overrides Sub Initialize(initializer As Object)
        m_filename= CStr(initializer)
    End Sub
    
    ' If the SoapMessageStage is such that the SoapRequest or SoapResponse
    ' is still in the SOAP format to be sent or received over the network,
    ' save it out to file.
    Public Overrides Sub ProcessMessage(message As SoapMessage)
        Select Case message.Stage
            Case SoapMessageStage.BeforeSerialize
            Case SoapMessageStage.AfterSerialize
                WriteOutput(message)
            Case SoapMessageStage.BeforeDeserialize
                WriteInput(message)
            Case SoapMessageStage.AfterDeserialize
            Case Else
                Throw New Exception("invalid stage")
        End Select
    End Sub
   
    ' Write the SOAP message out to a file.
    Public Sub WriteOutput(message As SoapMessage)
        newStream.Position = 0
        Dim fs As New FileStream(m_filename, FileMode.Append, _
                                 FileAccess.Write)
        Dim w As New StreamWriter(fs)
        w.WriteLine("-----Response at " + DateTime.Now.ToString())
        w.Flush()
        Copy(newStream, fs)
        w.Close()
        newStream.Position = 0
        Copy(newStream, oldStream)
    End Sub    
    
    ' Write the SOAP message out to a file.
    Public Sub WriteInput(message As SoapMessage)
        Copy(oldStream, newStream)
        Dim fs As New FileStream(m_filename, FileMode.Append, _
                                 FileAccess.Write)
        Dim w As New StreamWriter(fs)

        w.WriteLine("----- Request at " + DateTime.Now.ToString())
        w.Flush()
        newStream.Position = 0
        Copy(newStream, fs)
        w.Close()
        newStream.Position = 0
    End Sub    
    
    Sub Copy(fromStream As Stream, toStream As Stream)        
        Dim reader As New StreamReader(fromStream)
        Dim writer As New StreamWriter(toStream)
        writer.WriteLine(reader.ReadToEnd())
        writer.Flush()
    End Sub
End Class

' Create a SoapExtensionAttribute for our SOAP Extension that can be
' applied to an XML Web service method.
<AttributeUsage(AttributeTargets.Method)> _
Public Class TraceExtensionAttribute
    Inherits SoapExtensionAttribute
    
    Private m_filename As String = "c:\log.txt"
    Private m_priority As Integer    
    
    Public Overrides ReadOnly Property ExtensionType() As Type
        Get
            Return GetType(TraceExtension)
        End Get
    End Property 
    
    Public Overrides Property Priority() As Integer
        Get
            Return m_priority
        End Get
        Set
            m_priority = value
        End Set
    End Property 
    
    Public Property Filename() As String
        Get
            Return m_filename
        End Get
        Set
            m_filename= value
        End Set
    End Property
End Class
[C#]
  using System;
  using System.Web.Services;
  using System.Web.Services.Protocols;
  using System.IO;
  using System.Net;

  // Define a SOAP Extension that traces the SOAP request and SOAP
  // response for the XML Web service method the SOAP extension is
  // applied to.

  public class TraceExtension : SoapExtension 
  {
    Stream oldStream;
    Stream newStream;
    string filename;

    // Save the Stream representing the SOAP request or SOAP response into
    // a local memory buffer.
    public override Stream ChainStream( Stream stream ){
        oldStream = stream;
        newStream = new MemoryStream();
        return newStream;
    }

    // When the SOAP extension is accessed for the first time, the XML Web
    // service method it is applied to is accessed to store the file
    // name passed in, using the corresponding SoapExtensionAttribute.   
    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute) 
    {
        return ((TraceExtensionAttribute) attribute).Filename;
    }

    // The SOAP extension was configured to run using a configuration file
    // instead of an attribute applied to a specific XML Web service
    // method.
    public override object GetInitializer(Type WebServiceType) 
    {
      // Return a file name to log the trace information to, based on the
      // type.
      return "C:\\" + WebServiceType.FullName + ".log";    
    }

    // Receive the file name stored by GetInitializer and store it in a
    // member variable for this specific instance.
    public override void Initialize(object initializer) 
    {
        filename = (string) initializer;
    }

    //  If the SoapMessageStage is such that the SoapRequest or
    //  SoapResponse is still in the SOAP format to be sent or received,
    //  save it out to a file.
    public override void ProcessMessage(SoapMessage message) 
    {
        switch (message.Stage) {
        case SoapMessageStage.BeforeSerialize:
            break;
        case SoapMessageStage.AfterSerialize:
            WriteOutput(message);
            break;
        case SoapMessageStage.BeforeDeserialize:
            WriteInput(message);
            break;
        case SoapMessageStage.AfterDeserialize:
            break;
        default:
             throw new Exception("invalid stage");
        }
    }

    public void WriteOutput(SoapMessage message){
        newStream.Position = 0;
        FileStream fs = new FileStream(filename, FileMode.Append,
                                       FileAccess.Write);
        StreamWriter w = new StreamWriter(fs);

      string soapString = (message is SoapServerMessage) ? "SoapResponse" : "SoapRequest";
        w.WriteLine("-----" + soapString + " at " + DateTime.Now);
        w.Flush();
        Copy(newStream, fs);
        w.Close();
        newStream.Position = 0;
        Copy(newStream, oldStream);
    }

    public void WriteInput(SoapMessage message){
        Copy(oldStream, newStream);
        FileStream fs = new FileStream(filename, FileMode.Append,
                                       FileAccess.Write);
        StreamWriter w = new StreamWriter(fs);

        string soapString = (message is SoapServerMessage) ?
                            "SoapRequest" : "SoapResponse";
        w.WriteLine("-----" + soapString + 
                    " at " + DateTime.Now);
        w.Flush();
        newStream.Position = 0;
        Copy(newStream, fs);
        w.Close();
        newStream.Position = 0;
    }

    void Copy(Stream from, Stream to) 
    {
        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        writer.WriteLine(reader.ReadToEnd());
        writer.Flush();
    }
  }

   // Create a SoapExtensionAttribute for the SOAP Extension that can be
   // applied to an XML Web service method.
  [AttributeUsage(AttributeTargets.Method)]
  public class TraceExtensionAttribute : SoapExtensionAttribute {

    private string filename = "c:\\log.txt";
    private int priority;

    public override Type ExtensionType {
        get { return typeof(TraceExtension); }
    }

    public override int Priority {
        get { return priority; }
        set { priority = value; }
    }

    public string Filename {
        get {
            return filename;
        }
        set {
            filename = value;
        }
    }
  }

See Also

SoapExtension | SoapExtensionAttribute | SoapMessageStage | LogicalMethodInfo | Anatomy of an XML Web Service Lifetime | Configuring Applications | Building XML Web Services Using ASP.NET | Building XML Web Service Clients