Fun with SOAP Extensions

 

Keith Ballinger
Microsoft Corporation

March 22, 2001

Download or browse VBSoapEx.exe

***Note   *As promised, in this month's column we're going to look at one of the more advanced, but cooler features of ASP.NET Web ServicesSOAP Extensions. For this month's column Keith Ballinger, a Program Manager for .NET Web Services, has offered to share some of his knowledge of this subject. Thanks and see you next month. -Rob

One of the more interesting things you can do with the .NET Framework Web services technology is create SOAP Extensions. These extensions allow you to gain access to the actual network stream before it is deserialized into objects within the framework, and vice versa.

SOAP Extensions allow developers to create very interesting applications on top of the core SOAP architecture found within .NET. For instance, you can implement an encryption algorithm on top of the Web Service call. Alternatively, you could implement a compression routine, or even create a SOAP Extension that will accept SOAP Attachments.

How does this work? It's easy. First, I'd recommend that you review Rob Howard's earlier article Web Services with ASP.NET and then come back.

Basically, you need to do two things:

  1. Create a class that derives from System.Web.Services.Protocols.SoapExtension
  2. Create a class that derives from System.Web.Services.Protocols.SoapExtensionAttribute

And you are almost done! Now, all that you have to do is create something interesting from you derived classes. Ok, so maybe that isn't as easy as I make it sound. (Creating the interesting part is almost always the hardest part of development, isn't it?)

For this column, we will create an extension that records incoming and outgoing SOAP messages to our Web Service. This trace extension is useful for debugging when you really care about getting the SOAP message to look exactly the way you want it to look.

The core piece of implementation you need to worry about is the ProcessMessage method. This method is called several times, and every time it sends a SoapMessage object, it includes information on the stage of the SOAP message. To illustrate, let's examine the flow of a SOAP message within a Web Service. All of this applies on the client as well as the server, but we will concentrate on the server for this example.

  • A SOAP message comes in and the server figures out which method to route to.
  • Then the server checks to see if any SOAP extensions (that's us!) should be invoked, and if so, invokes them with the BeforeDeserialize event stage.
  • Last, the server deserializes the stream and invokes all the extensions for the AfterDeserialize stage.

The key for us here is the BeforeDeserialize event stage. Here, we grab the stream and write it out to the log file:

public void WriteInput( SoapMessage message ){

MemoryStream m = new MemoryStream();
   Copy( message.Stream, m);
   m.Seek(0, SeekOrigin.Begin);
   Double now = System.DateTime.Now.ToOADate();
FileStream fs = new FileStream("c:\\log.txt", FileMode.OpenOrCreate, FileAccess.Write);
   StreamWriter w = new StreamWriter(fs);         // create a Char writer 
   w.BaseStream.Seek(0, SeekOrigin.End);         // set the file pointer to the end
      
           TextReader reader = new StreamReader(m);
           String line;
           while ((line = reader.ReadLine()) != null) {
               w.WriteLine(line);
           }
           w.Flush();
           w.Close();

   m.Seek(0, SeekOrigin.Begin);
           message.Stream = m;
}

Notice we also write it out to a MemoryStream so that the SoapMessage object still has a valid stream when we are done. If we don't do this, the server will try to deserialize the network stream that was originally on the SoapMessage, and find nothing there.

A similar process takes place when we log the SOAP message that the server will return to the client. However, there is one exception. In the BeforeSerialize stage, we need to swap out the network stream on the SoapMessage, and replace it with one of our own MemoryStreams. This slight of hand will allow us to record whatever is sent as SOAP, without immediately losing it in the network.

public void SetStream( SoapMessage message ){
        oldStream = message.Stream;
        newStream = new MemoryStream();
        message.Stream = newStream;
    }

public void WriteOutput( SoapMessage message ){

   newStream.Seek(0, SeekOrigin.Begin);
   Double now = System.DateTime.Now.ToOADate();
          FileStream fs = new FileStream("c:\\log.txt", FileMode.OpenOrCreate, FileAccess.Write);
   StreamWriter w = new StreamWriter(fs);         // create a Char writer 
   w.BaseStream.Seek(0, SeekOrigin.End);         // set the file pointer to the end
      
           TextReader reader = new StreamReader(newStream);
           string line;
           while ((line = reader.ReadLine()) != null) {
               w.WriteLine(line);
           }
           w.Flush();
           w.Close();
           newStream.Seek(0, SeekOrigin.Begin);
   Copy( newStream, oldStream);
}

Notice that we record whatever was written to the memory stream, and then write it out to the network stream once we are done. If we don't write it out to the stream we stole, the client will never get a response.

Here is the full trace extension for you to use and play with:

<%@ WebService Language="c#" Class="TestIt" %>

using System;
using System.IO;
using System.Text;
using System.Web.Services;
using System.Web.Services.Protocols;

public class TestIt{

    [WebMethod]
   [TraceExtension()]
    public Address TestMethod() {
      Address address = new Address();
      address.Street = "123 Main St.";
      address.City = "Klamath Falls";
      address.State = "Oregon";
      address.ZipCode = "97600";
        return address;
    }
}

public class Address {
   public String Street;
   public String City;
   public String State;
   public String ZipCode;
}


[AttributeUsage(AttributeTargets.Method)]
public class TraceExtensionAttribute : SoapExtensionAttribute {

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

public class TraceExtension : SoapExtension {

    Stream oldStream;
    Stream newStream;

    public override int GetPriority() {
        return 0;
    }

    public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute) {
        return null;
    }

    public override void Initialize(object initializer) {
    }

    public override void ProcessMessage(SoapMessage message) {
        switch (message.Stage) {

        case SoapMessageStage.BeforeSerialize:
         SetStream( message );
            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 SetStream( SoapMessage message ){
        oldStream = message.Stream;
        newStream = new MemoryStream();
        message.Stream = newStream;
    }

   public void WriteOutput( SoapMessage message ){

      newStream.Seek(0, SeekOrigin.Begin);
      Double now = System.DateTime.Now.ToOADate();
          FileStream fs = new FileStream("c:\\log.txt", FileMode.OpenOrCreate, FileAccess.Write);
      StreamWriter w = new StreamWriter(fs);         // create a Char writer 
      w.BaseStream.Seek(0, SeekOrigin.End);         // set the file pointer to the end
      
        TextReader reader = new StreamReader(newStream);
        string line;
        while ((line = reader.ReadLine()) != null) {
            w.WriteLine(line);
        }
        w.Flush();
        w.Close();
        newStream.Seek(0, SeekOrigin.Begin);
      Copy( newStream, oldStream);
    }

    public void WriteInput( SoapMessage message ){

      MemoryStream m = new MemoryStream();
      Copy( message.Stream, m);

      m.Seek(0, SeekOrigin.Begin);
      Double now = System.DateTime.Now.ToOADate();
          FileStream fs = new FileStream("c:\\log.txt", FileMode.OpenOrCreate, FileAccess.Write);
      StreamWriter w = new StreamWriter(fs);         // create a Char writer 
      w.BaseStream.Seek(0, SeekOrigin.End);         // set the file pointer to the end
      
        TextReader reader = new StreamReader(m);
        string line;
        while ((line = reader.ReadLine()) != null) {
            w.WriteLine(line);
        }
        w.Flush();
        w.Close();

      m.Seek(0, SeekOrigin.Begin);
        message.Stream = m;
    }

    void Copy(Stream from, Stream to) {

        TextReader reader = new StreamReader(from);
        TextWriter writer = new StreamWriter(to);
        string line;
        while ((line = reader.ReadLine()) != null) {           
            writer.WriteLine(line);
        }
        writer.Flush();
    }
}

So there you have it—a trace extension. SOAP Extensions are a very useful feature, and I hope to see a lot imaginative uses for them over the next few years. There are some slight, but fundamental changes to the way extensions work in the upcoming beta 2 of the .NET Framework, but don't worry, we'll update this code (which is for beta 1) with beta 2 code when it comes out.