SOAP Extension – How does it work?

by Silan Liu

 

Soap extension is a mechanism provided by ASP.NET to customize the SOAP message received and sent out. There are four points in the whole message handling process of a called web method that we may want to costomize the SOAP message, as shown below:

The mechanism is to insert a soap message customizing class which inherits from SoapExtension on the message route, which takes in the soap message at one end, do some customization, and spit out the customized soap message at the other end. Suppose we want to do customization on the soap message before deserialize and after serialize, the diagram will be:

¨    The use of SoapExtensionAttribute

To insert the SoapExtension class into the input and output route of a web method, you need to have a corresponding SoapExtensionAttribute class to put before this web method, whose ExtensionType property tells the framework which SoapExtension class to use by returning the type of the SoapExtension. It also has a Priority property, so that when there are multiple SoapExtensionAttributes before a web method, the framework knows which SoapExtension processes the message first. In this case, the output of the first SoapExtension will become the input of the second SoapExtension. A new instance of the SoapExtension class is created for every web method call, and maintained for both the incoming and outgoing process of a method call.

The SoapExtensionAttribute is instantiated first time the web method is called, and cached for later calls.

The SoapExtensionAttribute class can have other properties that are used to initialize the corresponding SoapExtension object. They are assigned in the attribute brackets in front of the web method. Every time the web method is called, the framework retrieve the cached attribute object, create the corresponding SoapExtension object, and calls its Initialize method with this attribute object.

¨    The SoapExtension and the message’s round trip

A SoapExtension class is connected with two streams: one toward the wire direction, and one toward the application direction. A typical implementation of a SoapExtension class maintains a handle to the wire stream and a handle to the application stream. To connect a SoapExtension class to upstream and downstream, the framework calls its ChainStream method. A typical implementation of this method is:

    public class TestSoapExtension : System.Web.Services.Protocols.SoapExtension

    {

        private Stream mStreamToWire = null;

        private Stream mStreamToApplication = null;

 

        ...

 

        public override Stream ChainStream(Stream stream)

        {

            mStreamToWire = stream;

            mStreamToApplication = new MemoryStream();

            return mStreamToApplication;

        }

       

        ...

    }

The framework passes a handle of the wire stream to ChainStream, and ChainStream returns a handle of the application stream, which is a newly created MemoryStream.

The whole message round trip of the above scenario is shown below:

1.    The incoming soap message arrives. The framework instanciates and initializes the SoapExtension object.

2.    The framework calls the SoapExtension’s ChainStream method to connect the wire stream to it and get a newly created MemoryStream buffer.

3.    The framework calls the SoapExtension’s ProcessMessage method, which retrieves the soap message through the wire stream handle, does some customization, and write the customized message into the MemoryStream buffer through the application stream handle.

4.    This customized message contained in the MemoryStream buffer may go through further SoapExtensions, until it finally becomes an object in the application domain.

5.    After the returned object is serialized, the framework calls the ChainStream method of the same SoapExtension object to connect the wire stream to it and get a newly created MemoryStream buffer.

6.    The framework writes the soap message stream into the MemoryStream buffer returned by ChainStream.

7.    The framework calls the SoapExtension’s ProcessMessage method, which retrieves the soap message from the MemoryStream through the application stream handle, does some customization, and writes the customized message into the wire stream handle.

8.    The customized message may go through other SoapExtensions, until it reaches the wire.

¨    Wire stream is “read-only”

Note that the wire stream is read-only. You can not call its Seek method to move the pointer in the stream. Once you consume the stream, it is useless.

¨    How to create and use SoapExtension

To create a SoapExtension, simply create a class library with the SoapExtension and SoapExtensionAttribute class.

For a web service or its client to use the SoapExtension, make sure the class library dll is in the bin directory, and add a reference to it. Than, as the web service, add the corresponding SoapExtensionAttribute before the web method whose soap message you want to customize; as the client, add the same attribute before the proxy method.

The following example contains an example of a SoapExtension. The client sends a string “Hi!”. The first customization changes it to “Hi-1!”. The web method simply returns the string back. The second customization changes it to “Hi-2!”.

SoapExtensionAttribute class:

using System;

using System.Web.Services.Protocols;

 

namespace TestSoapExtension

{

    /// <summary>

    /// Summary description for TestSoapExtAtt.

    /// </summary>

    [AttributeUsage(AttributeTargets.Method)]

    public class TestSoapExtentionAttribute : SoapExtensionAttribute

    {

        private string mstrName = null;

 

        public override int Priority

        {

            get { return 1; }

            set { }

        }

 

        public override Type ExtensionType

        {

            get { return typeof(TestSoapExtension); }

        }

 

        public string Name

        {

            get { return mstrName; }

            set { mstrName = value; }

        }

    }

}

SoapExtension class:

using System;

using System.Web.Services.Protocols;

using System.IO;

 

namespace TestSoapExtension

{

    public class TestSoapExtension : System.Web.Services.Protocols.SoapExtension

    {

        private Stream mWireStream = null;

        private Stream mApplicationStream = null;

        private string mstrName = null;

 

        private const string strCallMsg =

            "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +

            "<soap:Envelope " +

               "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " +

               "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +

               "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +

               "<soap:Body>" +

                  "<Repeat xmlns=\"http://tempuri.org/\">" +

                    "<s>Hi-1!</s>" +

                  "</Repeat>" +

               "</soap:Body>" +

            "</soap:Envelope>";

 

        private const string strResponseMsg =

            "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +

            "<soap:Envelope " +

               "xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " +

               "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" " +

               "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +

               "<soap:Body>" +

                  "<RepeatResponse xmlns=\"http://tempuri.org/\">" +

                    "<RepeatResult>Hi-2!</RepeatResult>" +

                  "</RepeatResponse>" +

               "</soap:Body>" +

            "</soap:Envelope>";

 

        public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attr)

        {

            return (TestSoapExtentionAttribute) attr;

        }

 

        public override object GetInitializer(Type obj)

        {

            return obj;

        }

 

        public override void Initialize(object initializer)

        {

            mstrName = ((TestSoapExtentionAttribute)initializer).Name;

        }

 

        public override Stream ChainStream(Stream stream)

        {

            mWireStream = stream;

            mApplicationStream = new MemoryStream();

            return mApplicationStream;

        }

 

        public override void ProcessMessage(SoapMessage message)

        {

            StreamWriter writer = null;

            bool fIsServer = (message.GetType() == typeof(SoapServerMessage));

 

            switch (message.Stage)

            {

               case SoapMessageStage.BeforeDeserialize:

                  // Following block of code is the stream customization code. You must check

                  // whether it is invoked at the server side or client side, because it does

                  // different things at the two sides, e.g., encrypt and decrypt. 

                  writer = new StreamWriter(mApplicationStream);

                  if (fIsServer)

                    writer.WriteLine(strCallMsg);

                  else

                    writer.WriteLine(strResponseMsg);

                  writer.Flush();

 

                  mApplicationStream.Seek(0, SeekOrigin.Begin);

                  LogStream(mstrName + " - Before deserialize, IsServer = " + fIsServer.ToString(), mApplicationStream);

                  mApplicationStream.Seek(0, SeekOrigin.Begin);

                  break;

 

               case SoapMessageStage.AfterSerialize:

                  mApplicationStream.Seek(0, SeekOrigin.Begin);

                  LogStream(mstrName + " - AfterSerialize, IsServer = " + fIsServer.ToString(), mApplicationStream);

                  mApplicationStream.Seek(0, SeekOrigin.Begin);

 

                  // Following block of code is the stream customization code.

                  writer = new StreamWriter(mWireStream);

                  if (fIsServer)

                    writer.WriteLine(strResponseMsg);

                  else

                    writer.WriteLine(strCallMsg);

                  writer.Flush();

 

                  break;

            }

        }

 

        private void LogStream(string strTitle, Stream stream)

        {

            StreamReader sr = new StreamReader(stream);

            StreamWriter sw = new StreamWriter("c:\\temp\\log.txt", true);

            sw.Write("**** " + strTitle + "\r\n" + sr.ReadToEnd() + "\r\n\r\n");

            sw.Close();

            sw = null;

        }

    }

}

Web Service:

[WebMethod]

//[TestSoapExtentionAttribute(Name="First")]

public string Repeat(string s)

{

    StreamWriter sw = new StreamWriter("c:\\temp\\log.txt", true);

    sw.Write("**** Inside \"Repeat\": s = " + s + "\r\n\r\n");

    sw.Close();

    return s;

}

Web service proxy class at the client side:

[System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tempuri.org/Repeat",

    RequestNamespace="http://tempuri.org/",

    ResponseNamespace="http://tempuri.org/",

    Use=System.Web.Services.Description.SoapBindingUse.Literal,

    ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]

//[TestSoapExtentionAttribute(Name="First")]

public string Repeat(string s)

{

    object[] results = this.Invoke("Repeat", new object[] {

                s});

    return ((string)(results[0]));

}

Client:

using System;

using Client.localhost;

 

namespace Client

{

    class Class1

    {

        [STAThread]

        static void Main(string[] args)

        {

            try

            {

               Service1 s = new Service1();

               Console.WriteLine(s.Repeat("Hi!"));

            }

            catch (Exception ex)

            {

               Console.WriteLine(ex.ToString());

            }

 

            Console.ReadLine();

        }

    }

}

When the attribute is placed at the server side, the log file is:

**** First - Before deserialize, IsServer = True

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope

    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <soap:Body>

        <Repeat xmlns="http://tempuri.org/">

            <s>Hi-1!</s>

        </Repeat>

    </soap:Body>

</soap:Envelope>

 

**** Inside "Repeat": s = Hi-1!

 

**** First - AfterSerialize, IsServer = True

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope

    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <soap:Body>

        <RepeatResponse xmlns="http://tempuri.org/">

            <RepeatResult>Hi-1!</RepeatResult>

        </RepeatResponse>

    </soap:Body>

</soap:Envelope>

When the attribute is placed at the client side, the log file is:

**** First - AfterSerialize, IsServer = False

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope

    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <soap:Body>

        <Repeat xmlns="http://tempuri.org/">

            <s>Hi!</s>

        </Repeat>

    </soap:Body>

</soap:Envelope>

 

**** Inside "Repeat": s = Hi-1!

 

**** First - Before deserialize, IsServer = False

<?xml version="1.0" encoding="utf-8"?>

<soap:Envelope

    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xmlns:xsd="http://www.w3.org/2001/XMLSchema">

    <soap:Body>

        <RepeatResponse xmlns="http://tempuri.org/">

            <RepeatResult>Hi-2!</RepeatResult>

        </RepeatResponse>

    </soap:Body>

</soap:Envelope>