Creating a Custom SOAP Extension

In this section, we will take a look at how to create a custom SOAP extension for use with your source code. These custom extensions are useful for adding many things to your applications that do not make sense within the actual Web Method. For example, you may want some special processing to happen whenever a particular header comes through. If the header is used by many of your procedures and the processing for that header is always the same, a custom SOAP extension may be just the thing that you need. SOAP extensions have other uses as well. Some of those uses include the following:

  • Encrypting pieces of the message

  • Implementing extensions to SOAP, such as WS-Routing

  • Adding auditing of the Web Service

  • Anything else that you have to do to more than one message

The extensions can be used on clients and proxies. If you apply the extension to a client, you will want to create the proxy using the WSDL.EXE command-line tool instead of adding a Web Reference through Visual Studio .NET. Why? It is too easy to erase any changes to the Visual Studio .NET generated proxy, because all you have to do is right-click the Web Reference and select Update Web Reference. When you update the Web Reference, all of your changes are destroyed.

In this section, we will cover how custom SOAP extensions work. After this is described, we will create an actual extension. Let's start by covering the basics.

Custom SOAP Extension Basics

To implement a custom SOAP extension, you need to override two base classes from the System.Web.Services.Protocols namespace—SoapExtensionAttribute and SoapExtension. SoapExtensionAttribute provides the mechanism to attach the custom extension to a particular item. You can associate the attribute with anything that the System.AttributeTargets enumeration allows. Typically, these items will be attached to methods, classes, or structs. The class derived from SoapExtensionAttribute must override two properties—Priority and ExtensionType. The Priority property is used to indicate the relative priority of the extension with respect to other extensions. The priority influences the order in which the attributes will be applied. For example, you may want a cryptographic extension to decrypt data so that other custom SOAP extensions have access to the unencrypted data. An application that uses the attribute sets the priority through the web.config or app.config file. Listing 4.22 shows an entry that would modify the web.config file for the example project to set the Priority to 3.

Listing 4.22. Setting the Priority of the Example Extension to 3
<configuration>
    <system.web>
        <webServices>
            <soapExtensionTypes>
                <add type=
           "Ch4CustomAttribute.ReverseExtension, Ch4CustomAttribute"
                    priority="3" group="0" />
            </soapExtensionTypes>
        </webServices>
    </system.web>
</configuration>

This entry tells the framework that whenever it uses the attribute class that refers to Ch4CustomAttribute.ReverseExtension from the assembly named Ch4CustomAttribute, the extension is at priority 3 within group 0. The extension itself can be in group 0 or group 1. Group 0 has higher priority than any extension in group 1. Priority is then sorted within the groups where the lower numbers reflect a higher priority. Typically, you can avoid using this feature within this first version of ASP.NET. The default behavior works just fine. Besides that, my experience shows that setting this item within the config file typically results in unexpected behavior that is hard to debug. Unless you are applying a large number of extensions to one part of your Web Service, you should not use this feature.

The other property every override of SoapExtensionAttribute must implement is ExtensionType. ExtensionType returns the type that implements the workhorse of the SOAP Extension, the class that overrides SoapExtension. This class requires five overrides:

  • GetInitializer Two versions of this function exist. Both must be overridden. These functions allow a Web Service extension to do some one-time initialization. The first version takes a Type as a parameter and returns a value of type Object. This version gets called when the attribute applies to anything other than a Web Method. The other version passes a LogicalMethodInfo struct and an instance of the associated SoapExtensionAttribute-derived class. It also returns a value of type Object. This value is then used for each individual initialization of the class.

  • Initialize This function receives the object returned by the first call to one of the GetInitializer functions. You have no guarantees that the same instance of the class will call both GetInitializer and Initialize. As a result, do not depend on this happening and treat GetInitializer as if it belongs to a separate instance.

  • ChainStream This function passes in a Stream and returns a new Stream. Typically, you will save the reference to the stream passed in and return a stream of your own. When a message comes in, the passed in Stream contains the serialized message. Outgoing messages appear in the Stream returned by this method. The custom extension is responsible for copying data between Streams at the correct stage. (What's a stage? Keep reading.)

  • ProcessMessage This part actually handles the various stages of processing. As input, it takes a value from the SoapMessageStage enumeration. A message may go through either two or four stages, two for each direction the message travels. The BeforeDeserialize and AfterDeserialize stages handle the message as it comes in. If performing encryption of a message, you would use the BeforeDeserialize stage to decrypt the message before it is sent to the appropriate objects. I set AfterDeserialize after the message has been deserialized and just before the method itself gets called. The other pair of stages is BeforeSerialize and AfterSerialize. BeforeSerialize gets called just after the method gets called but before and serialization occurs. AfterSerialize gets called after the message is in XML format but before the message gets returned to the client.

Now that you know what these two classes do, it is time to create an example showing the collaboration in action.

An Example SOAP Extension

I could do a lot of different things for a SOAP extension. The .NET Framework SDK documents and many articles cover an extension that writes all messages out to a file. While that is a great example, something more people will need to do is actually manipulate the XML contained in the messages. To give you a feel for how to get into the XML and change the message contents without delving too deeply into concepts like cryptography, this section presents an extension that reverses the text in the first element in the Body of the message response. The code for this example is contained in the Ch4CustomAttribute project. Because the attribute will be used to reverse text, the two classes will be named ReverseExtensionAttribute and ReverseExtension.

ReverseExtensionAttribute derives from SoapExtensionAttribute and overrides the Priority and ExtensionType properties. If the extension was used for something more sophisticated, the attribute class would have other properties specific to it. An encryption attribute would likely contain a location of a key or the key itself to be used for encryption. A SOAP extension attribute can have as many or as few attributes as makes sense. Because our extension does very little, it contains no extra attributes. Listing 4.23 shows the code for the attribute class.

Listing 4.23. The ReverseExtensionAttribute Class
Imports System.Web.Services
Imports System.Web.Services.Protocols

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

    ' Stores the priority for the class
    Private m_priority As Integer
    ' Returns the type that inherits from
    ' SoapExtension
    Public Overrides ReadOnly Property ExtensionType() As Type
        Get
            Return GetType(ReverseExtension)
        End Get
    End Property

    ' Stores the Priority as set in the config file
    ' and returns that value on demand.
    Public Overrides Property Priority() As Integer
        Get
            Return m_priority
        End Get
        Set(ByVal Value As Integer)
            m_priority = Value
        End Set
    End Property

End Class
						

The attribute at the class declaration level declares that this class targets methods only. An attribute could target an entire Web Service or another item as well. This attribute associates itself with the SoapExtension derived class through the ExtensionType property. It tells ASP.NET that when a class uses this attribute, ASP.NET should use the ReverseExtension class to handle any SOAP requests that come through. Listing 4.24 shows the implementation of the ReverseExtension class.

Listing 4.24. The ReverseExtension Class
  1:  Imports System.Web.Services
  2:  Imports System
  3:  Imports System.Web.Services.Protocols
  4:  Imports System.IO
  5:  Imports System.Xml
  6:
  7:  Public Class ReverseExtension
  8:      Inherits SoapExtension
  9:
 10:      Private m_oldStream As Stream
 11:      Private m_newStream As Stream
 12:
 13:      ' Save the Stream representing the SOAP request or SOAP response into
 14:      ' a local memory buffer.
 15:      Public Overrides Function ChainStream(ByVal stream As Stream) As Stream
 16:          m_oldStream = stream
 17:          m_newStream = New MemoryStream()
 18:          Return m_newStream
 19:      End Function
 20:
 21:      ' Both GetInitializer overrides are present but do nothing.
 22:      Public Overloads Overrides Function GetInitializer( _
 23:          ByVal methodInfo As LogicalMethodInfo, _
 24:      ByVal attribute As SoapExtensionAttribute) As Object
 25:          ' No initializer used. By default, this returns Nothing
 26:      End Function
 27:
 28:      Public Overloads Overrides Function GetInitializer( _
 29:          ByVal WebServiceType As Type) As Object
 30:          ' No initializer used. By default, this returns Nothing
 31:      End Function
 32:
 33:      ' Implemented because it has to be but does nothing.
 34:      Public Overrides Sub Initialize(ByVal initializer As Object)
 35:          ' No initializer is used. No point in writing any actual
 36:          ' code.
 37:      End Sub
 38:
 39:      ' Handle any chaining of the message between old and new.
 40:      ' Besides that, manipulate the stream as needed
 41:      Public Overrides Sub ProcessMessage(ByVal message As SoapMessage)
 42:          Select Case message.Stage
 43:              Case SoapMessageStage.BeforeSerialize
 44:              Case SoapMessageStage.AfterSerialize
 45:                  HandleOutput()
 46:              Case SoapMessageStage.BeforeDeserialize
 47:                  HandleInput()
 48:              Case SoapMessageStage.AfterDeserialize
 49:              Case Else
 50:                  Throw New Exception("invalid stage")
 51:          End Select
 52:      End Sub
 53:
 54:      ' Reverse the contents of the first child of
 55:      ' the soap:Body element.
 56:      Public Sub HandleOutput()
 57:          Dim xmlDoc As New Xml.XmlDocument()
 58:          Dim xmlRdr As New StreamReader(m_newStream)
 59:
 60:          ' Read the stream into the XML Document
 61:          m_newStream.Position = 0
 62:          xmlDoc.LoadXml(xmlRdr.ReadToEnd())
 63:
 64:          ' Create a namespace manager. This way, whatever
 65:          ' the SOAP namespace is mapped to, we can refer
 66:          ' to it using a namepsace nomenclature that we
 67:          ' know will work.
 68:          Dim nsManager As New XmlNamespaceManager(xmlDoc.NameTable)
 69:
 70:          ' Map the XMLNS name "soap" to the correct URI.
 71:          nsManager.AddNamespace("soap", _
 72:              "http://schemas.xmlsoap.org/soap/envelope/")
 73:
 74:          ' Pick out the Body node.
 75:          Dim nodeBody As XmlNode = xmlDoc.SelectSingleNode( _
 76:              "/soap:Envelope/soap:Body", nsManager)
 77:          Dim value As String = _
 78:              nodeBody.FirstChild.FirstChild.InnerText
 79:
 80:          ' Reverse the contents of the first child of the response
 81:          ' element within the body.
 82:          nodeBody.FirstChild.FirstChild.InnerText = StrReverse(value)
 83:
 84:          ' Reset the length of the stream to 0.
 85:          m_newStream.SetLength(0)
 86:          Dim xmlWriter As New XmlTextWriter(m_newStream, _
 87:              New System.Text.UTF8Encoding())
 88:          xmlDoc.WriteContentTo(xmlWriter)
 89:          xmlWriter.Flush()
 90:          m_newStream.Position = 0
 91:
 92:          ' Chain this to the output: the old stream.
 93:          Copy(m_newStream, m_oldStream)
 94:      End Sub
 95:
 96:      ' Handle the physical chaining of the old stream and
 97:      ' the new stream that we created.
 98:      Public Sub HandleInput()
 99:          Copy(m_oldStream, m_newStream)
100:          m_newStream.Position = 0
101:      End Sub
102:
103:      Sub Copy(ByVal fromStream As Stream, ByVal toStream As Stream)
104:          Dim reader As New StreamReader(fromStream)
105:          Dim writer As New StreamWriter(toStream)
106:          writer.WriteLine(reader.ReadToEnd())
107:          writer.Flush()
108:      End Sub
109:  End Class
						

You will often use the code like that in Copy to help finish the chaining of the message streams. The only method in here that really does anything is HandleOutput. HandleInput just does the chaining in between streams to make sure the message gets processed correctly. In HandleOutput, the message from the m_newStream member variable is loaded into an XML document. The code then goes into the XML and looks for the element containing what will be the text “Hello World” in the sample Web Service. That text gets reversed within the XML. Finally, the XML is streamed back out to the m_newStream variable and copied to the m_oldStream, which gets returned to the original caller. To use the attribute, just apply it to any method that also uses the WebMethodAttribute class. Listing 4.25 shows a simple Hello World method that uses the attribute.

Listing 4.25. A Hello World Example That Uses the Custom SOAP Attribute
Imports System.Web.Services
<WebService(Namespace:="http://scottseely.com/")> _
Public Class Service1
    Inherits WebService

    <ReverseExtension(), _
    WebMethod()> Public Function HelloWorld() As String
        HelloWorld = "Hello World"
    End Function

End Class

Figure 4.3 shows the output of a client that uses the Web Service in Listing 4.25.

Figure 4.3. A simple message box that calls the Web Service in Listing 4.25.


The example is not exactly a general purpose attribute, but custom SOAP attributes don't have to be. If the attribute fills the need on a small set of methods and you want to write it, go for it. When does writing the attribute make sense? Any time you need to manipulate the XML, write an attribute. You can encrypt sections of the message, handle other extensions to the SOAP protocol, and so on. Attributes can be applied to the client and the server. Keep that in mind when working with Web Services.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset