The ASP Column
Using SOAP Extensions in ASP.NET
George Shepherd
Code download available at:ASPColumn0403.exe(136 KB)
Contents
Web Services and ASP.NET
Chaining the Message Stream
Life of the SOAP Message
Processing the Message
Deploying via Web.config
The SoapExtensionAttribute Class
Debugging the Extension
Conclusion
It's useful to be able to make a method call from one computer to another over the Internet. But if things stopped there, Web services would be a dead end. The ASP.NET SOAP extension framework represents a means of intercepting SOAP messages and hooking your own code into the SOAP message pipeline. Through the SOAP extension architecture, you have access to the message as it is deserialized into objects, and as it's serialized from a common language runtime (CLR) object back into a SOAP message. In this month's column I'm going to take a look at implementing SOAP extensions in ASP.NET.
SOAP extensions solve the problem of performing pre- and post-processing of SOAP messages. Some applications of this capability are obvious. For example, if you want to log SOAP messages as they're processed by your Web site, one of the best places to do it is within a SOAP extension. Some other applications may not be quite as obvious, but are just as useful. SOAP extensions are a means of transforming messages. If you want to secure a SOAP message, you could write SOAP extensions that encrypt and decrypt messages coming across the wire. Finally, the SOAP extension architecture represents a way to embellish the SOAP payload (for example, by sending attachments with a message).
In this column, I'll show you how to write a SOAP extension that logs the messages and reverses the text going across the wire. Many examples of SOAP extensions show how to log the requests, so I'll focus on modifying the message stream. The example illustrates a starting point, and you should see clearly how to insert your own extension-processing code.
Web Services and ASP.NET
Web services have evolved to allow global communication between computers. The software industry is settling upon SOAP as a common wire format and HTTP as the connection protocol. A Web service is basically a programmable Web site and represents a universal remote procedure call mechanism.
ASP.NET is built around a well-defined pipeline capable of responding to virtually any kind of HTTP request. If you think about it, a SOAP request, when bound to the HTTP protocol, is just another HTTP request (albeit one that contains a SOAP envelope). Web services fit cleanly into the ASP.NET pipeline, which provides a hook for extending messages. Figure 1 illustrates how SOAP extensions fit into the overall ASP.NET architecture.
Figure 1** ASP.NET Extension Architecture **
When a client sends SOAP messages to the ASP.NET Web service, it includes the name of the method in two places: in the SOAPAction header and the request element's name. By default, ASP.NET uses the SOAPAction header to figure out which method in the class to call (the method must be marked with the WebMethod attribute). The handler uses .NET reflection to find the method in the class being called by the client. After figuring out which WebMethod to call, the handler deserializes the SOAP message into .NET objects that can be supplied as method arguments during invocation.
ASP.NET automates much of this process, making it very easy to set up and run a Web service. However, if you want to see or modify the message stream coming across the wire, it's not exactly intuitive how to do so. That's where SOAP extensions come in.
ASP.NET SOAP extensions are implemented by classes deriving from the SoapExtension class. They allow you to intercept messages and modify the corresponding streams. You can have a full-fledged SOAP extension up and running by overriding a few simple functions. Let's take a look at the SoapExtension class.
The basis of SOAP extensions is the SoapExtension class. Fortunately, SoapExtension has a relatively small surface area. The main methods of concern are ChainStream, GetInitializer, Initialize, and ProcessMessage. ChainStream captures the message stream within the SOAP call. GetInitializer and Initialize provide a means of initialization. ProcessMessage is the heart of the extension where most of the development attention goes. I'll start the tour with the ChainStream method.
Chaining the Message Stream
Overriding the ChainStream method allows you to hook into the message stream. When an extension is installed, ASP.NET calls ChainStream, passing in a reference to the stream containing the SOAP message. The following code is typical of most implementations of ChainStream:
public override Stream ChainStream( Stream stream ) { // Save the message stream in a member variable. _originalStream = stream; // Create a new stream and save it as a member variable. _workingStream = new MemoryStream(); return _newStream; }
The main goal is to save the stream containing the SOAP message. This implementation also creates a new stream for holding a working copy of the message. The Stream passed into ChainStream contains the serialized SOAP request. SOAP extensions work by writing into a stream. Notice that ChainStream creates a memory stream and passes it back to the caller. The stream returned by ChainStream contains the serialized SOAP response.
You'll see later in this column how to deploy the extension. For now, you should just be aware that this method hooks the message stream.
Life of the SOAP Message
Once an extension is deployed with a project and the message stream is chained, SOAP requests and responses start to pass through it. There's a rather intricate lifecycle to the SOAP message, involving multiple stages of message processing. These stages are represented by the SoapMessageStage enumeration, and they include BeforeSerialize, AfterSerialize, BeforeDeserialize, and AfterDeserialize. This chain of events proceeds in a predefined sequence. Extensions can be installed with both the server and the client applications, and a similar sequence of events happens on each side. Figure 2 shows the path of a SOAP message through extensions deployed on both sides. The numbers 1-13 indicate the order in which the stages are processed.
Figure 2** Lifecycle of a SOAP Request **
Understanding what happens at each stage is key to making the extension work. During the entire transaction, the extension is running on both the client and server. For example, in Steps 2 and 3, the message is being prepared on the client as an outgoing message. A more detailed description of the processing stages follows.
SoapMessageStage.BeforeSerialize happens just before the SOAP message is serialized. This is Step 2 in the diagram. When a SOAP message is processed in client mode (the SOAP message is outgoing), the BeforeSerialize stage occurs immediately after a client invokes a Web service proxy method, but before the message is sent out over the wire. When the message is being processed in server mode (the SOAP message is incoming), BeforeSerialize occurs immediately after the WebMethod returns and before the return values are serialized and sent back to the client.
SoapMessageStage.BeforeDeserialize occurs before a message is deserialized and turned into a CLR object. When processing in client mode, BeforeDeserialize occurs after receiving the response from a WebMethod invocation, and just before the response is deserialized into a CLR object. When processing in server mode, BeforeDeserialize occurs after a SOAP message is received by the Web server, but before the SOAP message is deserialized into objects and passed as arguments to the WebMethod.
SoapMessageStage.AfterDeserialize occurs immediately after a SOAP message is deserialized into objects. When processing in client mode, the AfterDeserialize stage occurs after the response from the WebMethod invocation has been deserialized into an object, but prior to the client receiving the deserialized results. When processing in server mode, AfterDeserialize occurs after the request is deserialized into objects, but before the method on the object representing the Web service method is called.
SoapMessageStage.AfterSerialize occurs just after a SOAP message is serialized, but before the SOAP message is sent over the wire. When processing in client mode, AfterSerialize occurs after a client invokes a WebMethod on a client proxy and the parameters are serialized into XML, and before the SOAP message is sent over the wire. When processing in server mode, the AfterSerialize stage occurs after a WebMethod returns and values are serialized into XML, and before the SOAP message is sent over the network.
Processing the Message
The heart of the SOAP extension lies in the ProcessMessage method. ProcessMessage is called multiple times. Figure 3 shows the ProcessMessage code for this month's example.
Figure 3 Processing the SOAP Message Stages
public override void ProcessMessage(SoapMessage message) { switch (message.Stage) { // Incoming from client case SoapMessageStage.BeforeDeserialize: ReceiveStream(); LogInput(message); break; // About to call methods case SoapMessageStage.AfterDeserialize: break; // After Method call case SoapMessageStage.BeforeSerialize: break; // Outgoing to other case SoapMessageStage.AfterSerialize: LogOutput(message); ReturnStream(); break; default: throw new Exception("No stage such as this..."); } } void ReturnStream() { CopyAndReverse(_workingStream, _originalStream); } void ReceiveStream() { CopyAndReverse(_originalStream, _workingStream); } void CopyAndReverse(Stream from, Stream to) { TextReader tr = new StreamReader(from); TextWriter tw = new StreamWriter(to); string str = tr.ReadToEnd(); char[] data = str.ToCharArray(); Array.Reverse(data); string strReversed = new string(data); tw.Write(strReversed); tw.Flush(); }
Notice that this code simply switches on the different SOAP message stages. I'm not really concerned with BeforeSerialize, nor am I worried about AfterDeserialize in this case. BeforeDeserialize handles the including stream, copying and reversing the message, which is logged. The extension handles AfterSerialize by logging the message (before reversing it), then reversing the contents of the stream before returning it to the client.
I used a TCP tracing program to watch the SOAP calls going across the wire. Figure 4 shows the requests and responses. These are the actual bits as they are transmitted on the wire. You can see that the SOAP messages appear backward, showing that the SOAP extension worked.
Figure 4 Tracing SOAP Messages
HTTP Request
POST /UseSoapReverserExtension/Service1.asmx HTTP/1.1 User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol 1.0.3705.0) Content-Type: text/xml; charset=utf-8 SOAPAction: "https://tempuri.org/HelloWorld" Content-Length: 324 Expect: 100-continue Connection: Keep-Alive Host: localhost >epolevnE:paos/<>ydoB:paos/<>dlroWolleH/<>emaNrts/<egroeG>emaNrts<>"/ gro.irupmet//:ptth"=snlmx dlroWolleH<>ydoB:paos<>"amehcSLMX/1002/ gro.3w.www//:ptth"=dsx:snlmx "ecnatsni-amehcSLMX/1002/gro.3w.www// :ptth"=isx:snlmx "/epolevne/paos/gro.paoslmx.samehcs//:ptth"=paos:snlmx epolevnE:paos<>?"8-ftu"=gnidocne "0.1"=noisrev lmx?<
HTTP Response
HTTP/1.1 100 Continue Server: Microsoft-IIS/5.1 Date: Mon, 15 Dec 2003 21:02:08 GMT HTTP/1.1 200 OK Server: Microsoft-IIS/5.1 Date: Mon, 15 Dec 2003 21:02:08 GMT Cache-Control: private, max-age=0 Content-Type: text/xml; charset=utf-8 Content-Length: 371 >epolevnE:paos/<>ydoB:paos/<>esnopseRdlroWolleH/<>tluseRdlroWolleH/ <egroeG ,dlroW olleH>tluseRdlroWolleH<>"/gro.irupmet//:ptth"=snlmx esnopseRdlroWolleH<>ydoB:paos<>"amehcSLMX/1002/gro.3w.www// :ptth"=dsx:snlmx "ecnatsni-amehcSLMX/1002/gro.3w.www//:ptth"=isx:snlmx "/ epolevne/paos/gro.paoslmx.samehcs//:ptth"=paos:snlmx epolevnE:paos<>?"8- ftu"=gnidocne "0.1"=noisrev lmx?<
Deploying via Web.config
As with many of the ASP.NET extensibility points, SOAP extensions may be configured in web.config. If you mention the SOAP extension in your web.config file, ASP.NET will use it to process the WebMethods exposed by your service. Figure 5 shows how to declare a SOAP extension in web.config.
Figure 5 Declaring the SOAP Extension in Web.Config
<configuration> <system.web> <webServices> <soapExtensionTypes> <add type="SoapReverserExtensionLib.SoapReverserExtension, SoapReverserExtensionLib" priority="1" group="0" /> </soapExtensionTypes> </webServices> </system.web> </configuration>
The SoapExtensionAttribute Class
You may also configure singular WebMethods to run with your SOAP extension by adorning the WebMethod with a custom attribute derived from the SoapExtensionAttribute class. ASP.NET learns what kind of extension to use by querying the ExtensionType method of the attribute (which returns the type of extension to load). Figure 6 shows an example of a SOAP extension attribute for use with the SoapReverserExtension.
Figure 6 Defining a Custom SoapExtensionAttribute
[AttributeUsage(AttributeTargets.Method)] public class ReverserExtensionAttribute : SoapExtensionAttribute { private int priority; public override Type ExtensionType { get { // return type of extension to load return typeof(ReverserExtension); } } public override int Priority { get { return priority; } set { priority = value; } } } // Use the attribute like this: [WebMethod] [ReverserExtensionAttribute] public string HelloWorld(string strName) { return "Hello World, " + strName; }
You can apply the attribute to a particular WebMethod to configure the extension to be used with only that method as an alternative to configuring the extension through web.config, as shown in Figure 6. This will cause the extension to be used for all methods.
Debugging the Extension
Debugging a SOAP extension can be a bit different from how you might normally debug a Web service hosted in ASP.NET. ASP.NET uses the DefaultWsdlHelpGenerator.aspx page as configured in machine.config to display test pages for your Web services. These test pages can be used to invoke your WebMethods, but the test harness does this by making HTTP POST requests to the server rather than HTTP SOAP requests. SoapExtensions only work with SOAP requests, and thus any requests to your Web service made using the default test page will result in your extensions not being used.
To debug the extension, use either a custom SoapExtensionAttribute or a configuration setting in web.config to ensure that your extension will be used for the desired WebMethod. Set any appropriate breakpoints in your extension code. Press F5 to start debugging your Web service; Microsoft® Internet Explorer will load and display the test page for your Web service. Now, instead of using the test page to invoke your Web service, use another Web service client to invoke your method using a SOAP request (one such test tool is WebServiceStudio, available from GotDotNet). When the WebMethod is invoked, you'll see the debugger break on the breakpoints you've set.
Conclusion
This month I looked at a way to hook into the SOAP message stream using ASP.NET. While it's sometimes interesting to take a peek at the data coming across the wire, it's even more useful to be able to add functionality like logging, compression, and encryption. The basic idea of SOAP extensions is to intercept the message stream and work with it (encrypting the stream, adding to it, and so on). The trick to making it work is to understand the stages of SOAP message processing. And remember, you can write extensions to work only on the server, only on the client, or you can create them to work on both ends.
Send your questions and comments for George to asp-net@microsoft.com.
George Shepherd specializes in software development for the .NET Framework. He is the author of Programming with Microsoft Visual C++.NET (Microsoft Press, 2002), and coauthor of Applied .NET (Addison-Wesley, 2001). He teaches seminars with DevelopMentor and is a contributing architect for Syncfusion's .NET tools.