House of Web Services

Accessing Raw SOAP Messages in ASP.NET Web Services

Tim Ewald

Code download available at:WebServices0303.exe(54 KB)

Contents

Why Access Raw SOAP Messages?
Accessing Input and Output Streams
Exposing Input and Output Streams
A Prototypical Implementation
Piecing it All Together
What About Exceptions?
Why Bother with XML?

Web Services exchange XML messages. Most of today's Web Service toolkits do their best to hide this fact from developers, by exposing a Web Service's behavior as method invocations against objects instead. But what if you want access to the actual XML being sent on the wire? ASP.NET does not expose raw SOAP messages to a service, but with the built-in extensibility hooks and a little elbow grease, you can gain access to them.

But before I begin, let me make an observation about ASP.NET Web Services in general. When you develop Web Services in Visual Studio® .NET, the tool encourages you to use the ASP.NET plumbing a certain way. Specifically, it prompts you to implement a service as a class that exposes a set of methods marked with the [WebMethod] attribute, auto-generate a corresponding Web Services Description Language (WSDL) description and client proxy code, and then implement a client. However, there are other ways to use the Web Services infrastructure.

In his article "Place XML Message Design Ahead of Schema Planning to Improve Web Services Interoperability", Yasser Shohoud describes the importance of explicitly designing the messages a Web Service will send and receive. He argues that you should start the development of a Web Service by producing a contract in WSDL and then generating both service and client code from that, a view that I and many others share. The approach in which you write WSDL first takes advantage of the fact that the ASP.NET Web Services infrastructure is fairly modular and you only have to use the parts that you need. In this case, if you write your WSDL first, you don't need support for auto-generating it from your service code. I tend to think of the features of ASP.NET Web Services as falling into four large groups:

  • Extensible automatic WSDL generation
  • Message dispatching
  • Message filtering
  • Object serialization and deserialization

You can choose to embrace, ignore, or alter each piece as best suits your needs. Shohoud and many others choose to ignore the first one—automatic WSDL generation—as a basic design principle. The point is that the ASP.NET Web Services plumbing is flexible enough to let you make that choice. My goal in this and future columns is to explore these choices and to see how far you can push the Web Services plumbing of the Microsoft® .NET Framework. I'll start this column by showing you how to expose raw SOAP messages to a service, which is really a way of ignoring the object serialization and deserialization feature support for automatically mapping XML to objects.

Why Access Raw SOAP Messages?

The ASP.NET Web Services infrastructure assumes that, in general, developers prefer to deal with objects instead of XML. Its default behavior is to deserialize each SOAP message it receives from a client into objects and values that can be passed as input parameters to a method. After the method completes, it serializes the output parameters and return value into a SOAP message to send back to the client. The goal is to hide the use of XML.

But what if you want to deal with XML directly? For instance, maybe you want to query the messages you receive using XPath. Maybe your service is getting XML from SQL Server™ and you want to send it back to a client without having to turn it into objects, which the plumbing is just going to serialize back into XML anyway. Maybe you have a loosely coupled system that supports five different purchase order formats and you want to use XSLT to normalize them to your preferred internal format before continuing with the rest of your processing. Or maybe you are using objects that the ASP.NET Web Services plumbing can't serialize automatically, like a System.Collections.Hashtable or any implementation of IDictionary, and you want to serialize them by hand. In any of these cases, it would be better if you could manipulate SOAP messages using the System.Xml APIs of the .NET Framework.

The ASP.NET Web Services plumbing offers some limited support for accessing data as XML using pieces of the Document Object Model (DOM) API, notably System.Xml.XmlElement. While that's certainly a step in the right direction, it really doesn't go far enough. Optimally, any approach designed for exposing the SOAP messages that a Web Service produces and consumes as raw XML would work at the stream level. The System.Xml streaming APIs, called XmlReader and XmlWriter, are the basic plugs for XML I/O that all the higher-level APIs actually build on. If SOAP messages were exposed to streams, XmlReader and XmlWriter can be used to operate on them using the DOM, an XSLT transform, or any other XML technique.

Accessing Input and Output Streams

ASP.NET does not expose the streams containing inbound and outbound SOAP messages to a Web Service, but it will expose them to a SOAP extension. A SOAP extension is a filter that sits between the HTTP handler that processes requests sent to .asmx files and the class that actually implements a service. SOAP extensions can process an input message before a service's method is invoked and can process an output message after a service's method completes. In either case, the service is none the wiser.

Of key importance in the SOAP extension architecture is the ability to chain input and output streams. In the absence of any SOAP extensions, Web Services plumbing deals directly with the streams representing HTTP messages. A SOAP extension can replace the HTTP streams with streams of its own choosing, typically instances of System.IO.MemoryStream. The Web Service ends up working with the replacement streams and the plumbing leaves it to the SOAP extension to copy data between these streams and the "real" ones. "Real" is in quotes because any SOAP extension may in fact be dealing with replacement streams provided by another SOAP extension. This is the reason why extensions are said to "chain" streams.

Exposing Input and Output Streams

There is no reason that a SOAP extension cannot make the streams it works with available to a service. If it did, a service's methods would be able to read and write both input and output SOAP messages by hand. There is one challenge, however. If a method writes its own output SOAP message into the chained stream supplied by a SOAP extension and then the ASP.NET Web Service plumbing automatically generates an output SOAP message into the same stream, there will be a problem. The solution is to provide two output streams to the method, one for it to write into and one for the normal plumbing to use. The diagram in Figure 1 shows this basic architecture and flow.

Figure 1 Web Services Message Flow

Figure 1** Web Services Message Flow **

Normally, the ASP.NET Web Service deals directly with the HTTP input and output streams. In this case, however, the XmlStreamSoapExtension SOAP extension class chains the HTTP output stream and exposes a memory stream to the serialization layer instead. It also creates an additional output stream for the method to write its output SOAP message to. Note that the extension does not chain the input stream. Instead, it simply exposes the underlying HTTP stream directly. The service method accesses the input and output streams via the static properties of the SoapStreams class.

A Prototypical Implementation

The code in Figure 2 shows the implementation of XmlStreamSoapExtension (I omitted a couple of no-op operations for simplicity). The ChainStream method gives the SOAP extension a chance to replace the streams holding input and output messages. Since this extension wants to replace the output stream but not the input stream, it uses the output flag to determine which stream it is being asked to replace. A given SOAP extension object is always asked to chain the input stream before the output stream. The XmlStreamSoapExtension implementation uses the output flag—initially set to false—to skip the request to chain the input stream. This is done for efficiency; it prevents having to make an extra copy of the input message.

Figure 2 XMLStreamSoapExtension Implementation

// XmlStreamSoapExtension exposes raw SOAP messages to // an ASP.NET Web Service public class XmlStreamSoapExtension : SoapExtension { bool output = false; // flag indicating input or output Stream httpOutputStream; // HTTP output stream to send real output to Stream chainedOutputStream; // output stream for ASP.NET // plumbing to write to Stream appOutputStream; // output stream for method to write to ... // no-op operations omitted for simplicity // ChainStream replaces original stream with extension's stream public override Stream ChainStream(Stream stream) { Stream result = stream; // only replace output stream with memory stream if (output) { httpOutputStream = stream; result = chainedOutputStream = new MemoryStream(); } else { output = true; } return result; } // ProcessMessage is called to process SOAP messages // after inbound messages are deserialized to input // parameters and output parameters are serialized to // outbound messages public override void ProcessMessage(SoapMessage message) { switch (message.Stage) { case SoapMessageStage.AfterDeserialize : { // rewind HTTP input stream to start and store // reference in current HTTP context HttpContext.Current.Request.InputStream.Position = 0; HttpContext.Current.Items["SoapInputStream"] = HttpContext.Current.Request.InputStream; // create new memory stream for method to write // output message to and store reference in // current HTTP context appOutputStream = new MemoryStream(); HttpContext.Current.Items["SoapOutputStream"] = appOutputStream; break; } case SoapMessageStage.AfterSerialize : { // flush memory stream method wrote its output message to, rewind // it and copy it to the HTTP output stream appOutputStream.Flush(); appOutputStream.Position = 0; CopyStream(appOutputStream, httpOutputStream); appOutputStream.Close(); break; } } } // CopyStream copies the contents of a source stream // to a destination stream private void CopyStream(Stream src, Stream dest) { StreamReader reader = new StreamReader(src); StreamWriter writer = new StreamWriter(dest); writer.Write(reader.ReadToEnd()); writer.Flush(); } }

The ProcessMessage method is where the real work is done. ProcessMessage is invoked before and after an input message is deserialized to input parameters and before and after output parameters are serialized to an output message. After an input message is deserialized, the extension rewinds the HTTP input stream so a method can read it (the stream's cursor advances during deserialization) and creates a new memory stream for the method to write an output message to. This output stream, appOutputStream, is the second output stream required to make this scheme work. (The first output stream, chainedOutputStream, is created when ChainStream is called the second time.) References to both streams are stored in the current HTTP context's Items collection, with the keys "SoapInputStream" and "SoapOutputStream," respectively. After an output message is serialized, ProcessMessage copies the contents of the second output stream, appOutputStream, to the HTTP output stream—httpOutputStream. (Most SOAP extensions would copy the contents of their chained output stream to the HTTP output stream at this point.)

The CopyStream method is used simply to copy the data from one stream to another.

Two helper classes make it easier to use the XmlStreamSoapExtension class. The XmlStreamSoapExtensionAttribute class is used to mark a particular method as using this SOAP extension. The SoapStreams class exposes the input and output streams containing the raw SOAP messages to a method as a pair of static properties. The implementation of SoapStreams is shown in Figure 3.

Figure 3 SoapStreams Implementation

public class SoapStreams { public static Stream InputMessage { get { return (Stream) HttpContext.Current.Items["SoapInputStream"]; } } public static Stream OutputMessage { get { return (Stream) HttpContext.Current.Items["SoapOutputStream"]; } } }

Piecing it All Together

I've created a sample Web Service that adds two numbers. It's a simple example that shows how the XmlStreamSoapExtension works. The service's message formats are shown in Figure 4, expressed in XML Schema. These messages are used to implement a single operation called Add, described by the WSDL code in Figure 5.

Figure 5 Add Operation in WSDL

<wsdl:definitions xmlns:wsdl="https://schemas.xmlsoap.org/wsdl/" xmlns:soap="https://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:msdn-microsoft-com:hows" targetNamespace="urn:msdn-microsoft-com:hows" > <wsdl:import namespace="urn:msdn-microsoft-com:hows" location="server.xsd" /> <wsdl:types/> <wsdl:message name="Add"> <wsdl:part name="data" element="tns2:Add" /> </wsdl:message> <wsdl:message name="AddResponse"> <wsdl:part name="data" element="tns2:AddResponse" /> </wsdl:message> <wsdl:portType name="Arithmetic"> <wsdl:operation name="Add"> <wsdl:input message="tns:Add" /> <wsdl:output message="tns:AddResponse" /> </wsdl:operation> </wsdl:portType> <wsdl:binding name="Arithmetic" type="tns:Arithmetic"> <soap:binding transport=https://schemas.xmlsoap.org/soap/http style="document" /> <wsdl:operation name="Add"> <soap:operation soapAction="urn:msdn-microsoft-com:hows#Add" style="document" /> <wsdl:input message="tns:Add" > <soap:body use="literal" /> </wsdl:input> <wsdl:output message="tns:AddResponse" > <soap:body use="literal" /> </wsdl:output> </wsdl:operation> </wsdl:binding> </wsdl:definitions>

Figure 4 Web Service Message Formats

<xsd:schema xmlns:xsd="https://www.w3.org/2001/XMLSchema" targetNamespace="urn:msdn-microsoft-com:hows" elementFormDefault="qualified"> <xsd:element name="Add"> <xsd:complexType> <xsd:sequence> <xsd:element name="n1" type="xsd:int" /> <xsd:element name="n2" type="xsd:int" /> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="AddResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="sum" type="xsd:int" /> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema>

Figure 6 shows a sample service generated from that WSDL using the wsdl.exe command-line tool's /server switch and then modified. Note the presence of the [XmlStreamSoapExtension] attribute, which tells the ASP.NET plumbing to use the XmlStreamSoapExtension class to process messages that map to the Add method. Also note that the parameters to the method have been commented out; since it deals with SOAP messages directly, there is no need to serialize or deserialize any parameters. One side effect of this change is that you cannot auto-generate an accurate WSDL definition for this class. If you prefer writing WSDL first (as I do), this is no big deal. Alternatively, you could implement a SOAP extension reflector that modified the generated WSDL to reflect the XML formats a method expected to produce and consume (see Scott Short's article "Take Advantage of Existing External XML Schemas with a Custom Import Framework in ASP.NET" in the December 2002 issue of MSDN Magazine for more information).

Figure 6 Web Service Generated by WSDL

[WebServiceBinding(Name="Arithmetic", Namespace="urn:msdn-microsoft-com:hows")] public class Arithmetic : WebService { [XmlStreamSoapExtension] [WebMethod] [SoapDocumentMethod("urn:msdn-microsoft-com:hows#Add", Use=SoapBindingUse.Literal, ParameterStyle=SoapParameterStyle.Bare)] [return: XmlElement("AddResponse", Namespace="urn:msdn-microsoft-com:hows")] public /* AddResponse */ void Add(/*Add Add1*/) { // create validating reader with appropriate schemas XmlValidatingReader valid = new XmlValidatingReader( new XmlTextReader(SoapStreams.InputMessage)); valid.Schemas.Add(XmlSchema.Read(new XmlTextReader( HttpContext.Current.Server.MapPath("server.xsd")), null)); // load and validate input message XmlDocument doc = new XmlDocument(); doc.Load(valid); // create transform and load XSLT XslTransform transform = new XslTransform(); transform.Load(new XmlTextReader( HttpContext.Current.Server.MapPath("add.xslt"))); // process input message and generate the output message transform.Transform(doc, null, new XmlTextWriter( SoapStreams.OutputMessage, Encoding.UTF8)); } }

The implementation of Add simply retrieves the input message stream using SoapStreams.InputMessage and parses it into a DOM, validating it against the service's schema along the way (something the ASP.NET Web Service plumbing does not do by default). Then it applies an XSLT to generate the output message, which it writes to SoapStreams.OutputMessage. Here is the transform the method uses, called add.xslt:

<soap:Envelope xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/" xmlns:xsl="https://www.w3.org/1999/XSL/Transform" xsl:version="1.0"> <soap:Body> <ns:AddResponse xmlns:ns="urn:msdn-microsoft-com:hows"> <ns:sum><xsl:value-of select="sum(//ns:Add/*/text())"/></ns:sum> </ns:AddResponse> </soap:Body> </soap:Envelope>

What About Exceptions?

There is one more thing to think about: what happens if an exception occurs? Normally, the ASP.NET Web Services infrastructure maps exceptions to SOAP Faults. But the implementation of XmlStreamSoapExtension.ProcessMessage ignores the stream containing the output SOAP message generated by that plumbing (chainedOutputStream) and uses the one created by the method instead (the one in appOutputStream). This is a problem.

There are two possible solutions. One is to insist that every method that uses the XmlStreamSoapExtension handle exceptions itself, producing an output SOAP message containing a SOAP Fault element if necessary. The other is to add some logic to the extension's ProcessMessage method that looks for a SOAP Fault element in the output message generated by the plumbing. If a Fault is present, an exception was thrown and the message containing the Fault should be returned. Figure 7 shows an updated version of XmlStreamSoapExtension.ProcessMessage to do that.

Figure 7 Looking for a SOAP Fault

// ProcessMessage is called to process SOAP messages after inbound messages // are deserialized to input parameters and output parameters are // serialized to outbound messages public override void ProcessMessage(SoapMessage message) { switch (message.Stage) { case SoapMessageStage.AfterDeserialize : { // rewind HTTP input stream to start and store // reference in current HTTP context HttpContext.Current.Request.InputStream.Position = 0; HttpContext.Current.Items["SoapInputStream"] = HttpContext.Current.Request.InputStream; // create new memory stream for method to write output message to // and store reference in current HTTP context appOutputStream = new MemoryStream(); HttpContext.Current.Items["SoapOutputStream"] = appOutputStream; break; } case SoapMessageStage.AfterSerialize : { // scan chained output stream looking for SOAP fault chainedOutputStream.Position = 0; XmlReader reader = new XmlTextReader(chainedOutputStream); reader.ReadStartElement("Envelope", "https://schemas.xmlsoap.org/soap/envelope/"); reader.MoveToContent(); if (reader.LocalName == "Header") reader.Skip(); reader.ReadStartElement("Body", "https://schemas.xmlsoap.org/soap/envelope/"); reader.MoveToContent(); if (reader.LocalName == "Fault" && reader.NamespaceURI == "https://schemas.xmlsoap.org/soap/envelope/") { // fault was found, so rewind chained output stream // and copy it to HTTP output stream chainedOutputStream.Position = 0; CopyStream(chainedOutputStream, httpOutputStream); } else { // No fault was found, so flush memory stream that method wrote its // output message to, rewind it and copy it to the HTTP output stream appOutputStream.Flush(); appOutputStream.Position = 0; CopyStream(appOutputStream, httpOutputStream); } appOutputStream.Close(); break; } } }

Why Bother with XML?

It is a testament to the extensibility of the ASP.NET Web Services infrastructure that with a fairly simple SOAP extension, you can expose raw SOAP messages to the methods of a service class. Why bother? SOAP and Web Services are XML technologies. While it is often convenient to implement Web Services using objects, hiding the underlying XML means you have to give up some very powerful tools, like XPath, XSLT, and XML Query. Working at the XML level is probably unnecessary when you are adding two numbers together (although it was nice to get support for XML Schema validation), but that's really not the point. The point is that in many cases working with XML is actually easier than working with objects and it is nice to be able to deal directly with SOAP messages when your app requires it. You may wonder why I didn't just go all the way and implement my own Web Service endpoint using a low-level HTTP handler (an approach I've taken in the past). The main advantage of working inside (for the most part) of the ASP.NET Web Services infrastructure is that you can also take advantage of the object abstraction when it makes sense, a feature that a plain vanilla HTTP solution wouldn't provide.

Got a question? Send questions and comments to housews@microsoft.com.

Tim Ewaldis a Program Manager for XML Web Services at Microsoft. He is currently redesigning MSDN's architecture around Web services, as well as writing and speaking about the technology. He is the author of Transactional COM+: Designing Scalable Applications (Addison-Wesley, 2001). Reach Tim at tewald@microsoft.com.