.NET Web Services III...

Part III: SOAP Extensions


By: Chris Sully Date: September 20, 2003

Introduction

After reviewing the basics of Web Services in Part I and continuing to look at more advanced specifics in part II, in this article and the next we continue the progression looking at three more advanced Web Service related activities:

SOAP extensions allow you to modify the SOAP messages sent between client and server by inserting your own custom code into the message as it is created. Alternatively/ additionally they allow you to perform other processing at set points in the Web Services lifecycle. SOAP extensions are usually deployed in pairs with matching extensions on client and server. This is quite a complex area and shall be a sufficient topic for this article.

Asynchronous calls, as introduced in article II, are a client side technique for making more efficient use of Web Services. An asynchronous call does not wait for the result to be returned by the Web Service before proceeding with its work. The call is made, the client proceeds with its work and is notified when the result arrives, which it can then handle as and when it sees fit. The proxy class automatically enables asynchronicity but there are various related techniques for exploration.

Custom wire formatting allows the developer to specify the format of the SOAP messages used by the Web Service. This may be necessary if client software is expecting messages in a particular format. The formatting is achieved via the use of attributes within your code.

Creating and using SOAP extensions

A SOAP extension primarily facilitates processing of the SOAP messages. Hence tasks such as encryption or signature verification may be performed. How does this fit into the Web Services architecture? When a Web Service client invokes a Web method from a Web Service Server, the client takes the objects and parameters involved and converts them into an XML message – the SOAP request. This process of converting objects to XML is termed serialization, with the reverse process being deserialization. To complete the cycle the SOAP message must be deserialized when it reaches the server, then the SOAP response must be serialized at the server before being sent and then deserialized at the client.

You may insert SOAP extensions into this process before or after any serialisation or deserialization activity – a total of 8 locations if you count them up. It is also quite permissible to have multiple SOAP extensions working at a time, in which case you are also able to assign a priority to the application of each extension.

Where and in which combination you implement the SOAP extensions depends on the functionality you are achieving. For example:

Writing server-side and client-side SOAP extensions are similar endeavours but we shall focus on the former initially.

The tasks that may be required of the SOAP extension code are:

  1. Derive a new class from the SoapExtension class.
  2. Implement the GetInitializer and Initialize methods to handle initialization activities.
  3. Implement the ChainStream method to intercept the SOAP messages as they are serialized/ deserialized.
  4. Implement the ProcessMessage method to work with the SOAP messages.
  5. Derive a class from the SoapExtensionAttribute class to allow indication of methods that will use the extension.

Now, what example to choose? We need an example that is simple enough to introduce here while covering the main aspects of the required implementation. The example will log incoming and outgoing messages to a disk file. It is based on an example presented in Mike Gunderloy’s 'Developing XML Web Services and Server Components with VB.NET and the .NET Framework', a book I can recommend, particularly if studying for Microsoft exam 70-310.

The server side solution involves the Web Service itself as well as the necessary supporting files for the SOAP extension: SoapDisplayExtension.vb and SoapDisplayExtensionAttribute.vb, the latter necessary so we can associate the extension with the method. Additionally we need a Windows client so we can test the SOAP extension.

We'll start with the simple code of tracker.asmx – the Web Service. Note the WebService itself isn't going to do a great deal, just return a string, so the reader can focus on the SOAP extension detail.

Imports System.Web.Services

<WebService(Namespace:="http://tracker.cymru-web.net/")> _
Public Class Tracker
  Inherits System.Web.Services.WebService

  <SoapDisplayExtension(), WebMethod()> _
  Public Function GetMessage(ByVal ID As Integer) As String
    GetMessage = "This is a message returned from the tracker Web Service, ID= " & ID
  End Function

End Class

All as you'd expect except perhaps the additional attribute of the WebMethod that we define via a separate class in the project, SoapDisplayExtensionAttribute.vb, which we'll return to shortly.

The main file we need is that which defines the SOAP extension itself, SoapDisplayExtension.vb:

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

Public Class SoapDisplayExtension
  Inherits SoapExtension

  ' Variables to hold the original stream
  ' and the stream that we return
  Private originalStream As Stream
  Private internalStream As Stream

  ' Called the first time the Web service is used
  ' Version called if configured with an attribute
  Public Overloads Overrides Function GetInitializer( _
    ByVal methodInfo As LogicalMethodInfo, _
    ByVal attribute As SoapExtensionAttribute) As Object
      ' Not used in this example, but it's declared
      ' MustOverride in the base class
  End Function
  ' Version called if configured with a config file
  Public Overloads Overrides Function GetInitializer( _
  ByVal WebServiceType As Type) As Object
    ' Not used in this example, but it's declared
    ' MustOverride in the base class
  End Function

  ' Called each time the Web service is used
  ' And gets passed the data from GetInitializer
  Public Overrides Sub Initialize( _
  ByVal initializer As Object)
    ' Not used in this example, but it's declared
    ' MustOverride in the base class
  End Sub

  ' The Chainstream method gives us a chance
  ' to grab the SOAP messages as they go by
  Public Overrides Function ChainStream( _
    ByVal stream As Stream) As Stream
    ' Save the original stream
    originalStream = stream
    ' Create and return our own in its place
    internalStream = New MemoryStream()
    ChainStream = internalStream
  End Function

  ' The ProcessMessage method is where we do our work
  Public Overrides Sub ProcessMessage( _
    ByVal message As SoapMessage)
    ' Determine the stage and take appropriate action
    Select Case message.Stage
      Case SoapMessageStage.BeforeSerialize
        ' About to prepare a SOAP Response
      Case SoapMessageStage.AfterSerialize
        ' SOAP response is all prepared
        ' Open a log file and write a status line
        Dim fs As FileStream = _
          New FileStream("c:\temp\Tracker.log", _
          FileMode.Append, FileAccess.Write)
        Dim sw As New StreamWriter(fs)
        sw.WriteLine("AfterSerialize")
        sw.Flush()
        ' Copy the passed message to the file
        internalStream.Position = 0
        CopyStream(internalStream, fs)
        fs.Close()
        ' Copy the passed message to the other stream
        internalStream.Position = 0
        CopyStream(internalStream, originalStream)
        internalStream.Position = 0
      Case SoapMessageStage.BeforeDeserialize
        ' About to handle a SOAP request
        ' Copy the passed message to the other stream
        CopyStream(originalStream, internalStream)
        internalStream.Position = 0
        ' Open a log file and write a status line
        Dim fs As FileStream = _
        New FileStream("c:\temp\Tracker.log", _
          FileMode.Append, FileAccess.Write)
        Dim sw As New StreamWriter(fs)
        sw.WriteLine("BeforeDeserialize")
        sw.Flush()
        ' Copy the passed message to the file
        CopyStream(internalStream, fs)
        fs.Close()
        internalStream.Position = 0
      Case SoapMessageStage.AfterDeserialize
        ' SOAP request has been deserialized
    End Select
  End Sub

  ' Helper function to copy one stream to another
  Private Sub CopyStream(ByVal fromStream As Stream, _
    ByVal toStream As Stream)
    Try
      Dim sr As New StreamReader(fromStream)
      Dim sw As New StreamWriter(toStream)
      sw.WriteLine(sr.ReadToEnd())
      sw.Flush()
    Catch ex As Exception
    End Try
  End Sub
End Class

Work your way through the above noting how it achieves tasks 1 through 4 above:

  1. We drive a new class from the SoapExtension class after importing the required namespaces.
  2. Implement the GetInitializer and Initialize methods to handle initialization activities. We don't actually use them in this example but as they are declared with MustOverride in the base class we must implement.
  3. Implement the ChainStream method to intercept the SOAP messages as they are serialized/ deserialized. We simply replace the original stream with our newly created stream via the ChainStream implementation.
  4. Implement the ProcessMessage method to work with the SOAP messages. This is the key area of implementation in this example. Via the Case statement based on the Stage property of the SoapMessage, we choose to undertake some processing after serialization (the SOAP response is ready) and before deserialization (the SOAP request is about to be handled). In both cases we’re simply writing a status message and the SOAP stream to the log file.

Returning to SoapDisplayExtensionAttribute.vb which links the method to the extension:

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

<AttributeUsage(AttributeTargets.Method)> _
Public Class SoapDisplayExtensionAttribute
  Inherits SoapExtensionAttribute

  Private m_Priority As Integer = 1

  ' Specifies the class of the SOAP
  ' Extension to use with this method
  Public Overrides ReadOnly Property ExtensionType() As Type
    Get
      ExtensionType = GetType(SoapDisplayExtension)
    End Get
  End Property

  ' Member to store the extension's priority
  Public Overrides Property Priority() As Integer
    Get
      Priority = m_Priority
    End Get
    Set(ByVal Value As Integer)
      m_Priority = Value
    End Set
  End Property

End Class

When a request comes in for the GetMessage function the Web Service recognises the declared SoapDisplayExtension attribute:

<SoapDisplayExtension(), WebMethod()> _
Public Function GetMessage(ByVal ID As Integer) As String
  GetMessage = "This is a message returned from the tracker Web Service, ID= " & ID
End Function

and routes the request through the SoapDisplayExtension class as dictated by the SoapDisplayExtensionAttribute class. Within this class the SOAP messages are logged to a disk file before being passed on.

To prove this is all working we need a client. I implemented a Windows Form client with button and textbox controls named btnCallWebService and txtOutout respectively with the following simple code behind the form:

Private Sub btnCallWebService_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnCallWebService.Click
  Dim track As localhost.Tracker = New localhost.Tracker
  txtOutput.Text = track.GetMessage(1)
End Sub

Remebering to add your Web Reference to the Web Service itself.

Run the Windows form project, click the button and you’ll receive a response. Now open up the log file in an appropriate editor and you'll see something along the lines of:

BeforeDeserialize
<?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><GetMessage
xmlns="http://tracker.cymru-web.net/">
<ID>1</ID></GetMessage></soap:Body></soap:Envelope>

AfterSerialize
<?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><GetMessageResponse
xmlns="http://tracker.cymru-web.net/"><GetMessageResult>This is a message returned
from the tracker Web Service, ID= 1
</GetMessageResult></GetMessageResponse></soap:Body></soap:Envelope>

It worked!

Writing a client-side SOAP extension

The process of writing a client-side SOAP extension is similar. The main difference is that you must add the SoapDisplayExtension attribute to the proxy class of the Web Service rather than the Web Service itself. The proxy class in VS.NET is located in the Reference.vb in the web references folder. For our example above the code will be similar to:

<System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tracker.cymru-web.net/GetMessage", RequestNamespace:="http://tracker.cymru-web.net/", ResponseNamespace:="http://tracker.cymru-web.net/", Use:=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle:=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)> _
  Public Function GetMessage(ByVal ID As Integer) As String
    Dim results() As Object = Me.Invoke("GetMessage", New Object() {ID})
    Return CType(results(0),String)
  End Function

and you just need to add one line prior to the above so it becomes:

<SoapDislayExtension(), _
System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tracker.cymru-
etc.

Finally, remember that the client serializes requests and deserializes responses, whereas the server deserializes requests and serializes responses.

Conclusion

As we've started to delve deeper into the complexities of Web Services with this topic you'll need to explore further to appreciate the ins and outs but I hope this has provided an overview of the key elements of the process. In the next article the subject matter will be a little less complex when we return to asynchronous web methods as well as looking how we can control the XML wire format and why we might want to.

References

.NET SDK

Developing XML Web Services and Server Components with VB.NET and the .NET Framework
Mike Gunderloy
Que