Sponsored Links


Resources

.NET Research Library
Get .NET related white papers, case studies and webcasts

SOAP ExtensionsSOAP ExtensionsSOAP Extensions Discuss DiscussDiscuss Printer friendly Printer friendlyPrinter friendly
SOAP Extensions

August 19, 2004

Introduction

Web Services exchange XML messages. The WSDL specification allows for Web Services to be implemented in two different styles, namely, RPC and document and allows two different types of serialization, namely, encoding and literal. Most Web Services are built using the RPC-style with SOAP encoding. This approach seems to be well suited for exposing the methods of existing classes as Web Services. However, Web Services based on document/literal style are better suited while building applications that have to interact by “exchanging documents” rather than by “invoking methods”. Typically, a Web Service is implemented as a class which exposes a set of methods that take one or more objects as parameters. This “object-oriented” approach involves mapping the underlying XML data by ASP.NET plumbing into a hierarchy of objects. However, by making use of SOAP Extensions in .NET, one can work directly with SOAP messages without having to deal with “objects”. This article presents a study of how this feature has been taken advantage of in a recent project.

Design Requirements

The client-side of the application receives data sent in a healthcare industry standard format called HL7 (Health Level 7) and first transforms it to an XML document. The client-side application has to then transmit this XML document to a server-side application, separated by the network, by making use of a Web Service. The XML document must conform to a given schema. The server-side application then parses and processes the XML document. The size of XML documents to be transmitted by the client could be very large, causing an object representation of XML data to result in a potentially large memory footprint. Hence, it is better to work directly with the XML data while invoking the Web Service. The same principle applies to the server-side when it receives the XML data wrapped in a SOAP message.

The primary benefit of using document/literal based messaging in such a Web Service is that the XML schema associated with the WSDL file completely describes the contents of <soap:Body> element in the SOAP message. Hence, the associated schema may be used to validate the message body without any additional rules. However, with rpc/encoding based messaging, the schema is insufficient to completely describe the contents of <soap:Body> as we must also know the RPC encoding rules.

The complete description of a simplified Web Service in WSDL is shown in Figure 1. A simplified sample of the XML document that is to be transmitted to the server-side is shown in Figure 2. Inspection of these documents shows XML data is completely described by the schema that appears within the <wsdl:types> section of the service description in WSDL.

Object-oriented Approach

Even though the SOAP body of document/literal based messages contain XML document without any encoding rules, in order to invoke the Web Service method the client would still have to pass in a reference to an object. If the WSDL tool (wsdl.exe) is used to generate code for the stub and proxy based on the description shown in Figure 1., it creates the classes shown below for the server and client sides.

// Server-side Stub Class; The Web Service Implementation Derives from 
	this Abstract Class
public abstract class TransferService : System.Web.Services.WebService {
public abstract TransferResponse transfer (Patient[] PatientList);
}

// Client-side Proxy Class
public class TransferService : 
	System.Web.Services.Protocols.SoapHttpClientProtocol {
public TransferResponse transfer(Patient[] PatientList) {...}
}
// Other Helper Classes Based on the Types Defined in the XML Schema
public class Patient {...}
public class Name {...}
public enum Gender {male, female}
public class TransferResponse {}

The WSDL tool generates classes that correspond to each one of the user-defined types in the XML schema, namely, Patient, Name and Gender. If Visual Studio .NET is used to auto-generate the proxies by using the Add Web Reference feature, it does the same as well. Using the above client-side proxy, the operation transferData on the Web Service would have to be invoked as follows, while programming in Visual C# .NET:

// Create a Reference to a Web Service Proxy
TransferService intf= new TransferService ();

// Get an Instance of the Class that Maps to the  Element in Schema
Patient[] listOfPatients;
intf.transferData (listOfPatients);

The parameter listOfPatients passed to the transferData operation should map to the schema element named PatientList defined in the WSDL description. This parameter is an object of type Patient[]. We are forced to do this type of method invocation as the Web Services infrastructure assumes that we prefer to deal with objects instead of XML documents. Thus, starting with the XML document shown in Figure 1., we have to create a list of Patient objects using the helper classes generated by the WSDL tool and pass an array of these objects as the argument to the service method invocation. Doing so would result in this XML document being carried as the payload within the <soap:Body> element of the SOAP message.

If the XML document were to comprise thousands of such <patient> elements and if each <patient> element comprised several child elements, it is easy to envision how the system would then demand a large amount of memory, having to instantiate thousands of objects. Such a mapping of XML data into a hierarchy of objects is an unnecessary step if all that the client desires to do is send a schema-compliant XML document to the server-side. This is where we make use of the SOAP Extensions.

Using SOAP Extensions

A SOAP extension can be injected into the Web Services infrastructure to inspect or modify the SOAP messages before they are transmitted. As we are dealing with raw XML data, it allows us to manipulate SOAP messages using the System.Xml APIs of the .NET Framework. A SOAP extension is a derived from System.Web.Services.Protocols.SoapExtension. The derived implementation should override the ChainStream method which is invoked by ASP.NET plumbing and enables a SOAP extension to receive a reference to the stream that holds input and output messages. The real work in a SOAP extension is typically done in the overridden ProcessMessage method which is invoked by the ASP.NET plumbing at four different stages, defined in the System.Web.Services.Protocols.SoapMessageStage enumeration, namely, BeforeSerialize, AfterSerialize, BeforeDeserialize and AfterDeserialize. There are couple of other methods that need to be overridden but these method can be left as no-op methods and are not discussed here.

Client-side SOAP Extension

The SOAP extension on the client-side of our application needs to modify the outgoing messages and hence it saves a reference to the output Stream passed into the ChainStream method. Further, it allocates and returns an instance MemoryStream which will be used by ASP.NET plumbing for its data serialization. The implementation of ChainStream method is shown below.

public class ClientSideSoapExtension: SoapExtension {

private bool outgoing= true;
private Stream outputStream;
private Stream chainedOutputStream;

public override Stream ChainStream (Stream stream) {
	Stream result = stream;
	if (this.outgoing) {
		this. outputStream = stream;
		this.chainedOutputStream = new MemoryStream();
		result = this.chainedOutputStream;
		this.outgoing = false;
	}
	return result;
	}
}
}

The SOAP extension on the client-side of our application does its work in the stage that corresponds to SoapMessageStage.AfterSerialize. This stage occurs after the ASP.NET plumbing has serialized the input parameters to the service method into the stream that was provided to it in the ChainStream method. Here, the SOAP extension merely retrieves the XML document from cache (the details of how this is done are not relevant here) and loads into the output stream, a reference to which was saved in the ChainStream method, as shown below:

public class ClientSideSoapExtension : SoapExtension {
public override void ProcessMessage (SoapMessage message) {

	switch (message.Stage) {		
	case SoapMessageStage.BeforeSerialize : 
		break;
		
	case SoapMessageStage.AfterSerialize : {

		// Get the XML string which goes into the SOAP body
		String soapBodyString= getXMLFromCache ();
	
		// Create the SOAP Message 
		// It Comprises of a <soap:Element> that Enclosed a <soap:Body>. 
		// Pack the XML  Document Inside the <soap:Body> Element	
		String xmlVersionString= "<?xml version=\"1.0\" 
			encoding=\"utf-8\"?>";
		String soapEnvelopeBeginString= "<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\">";
		String soapBodyBeginString= "<soap:Body>";
		String soapBodyEndString= "</soap:Body>";
		String soapEnvelopeEndString= "</soap:Envelope>";

		Stream appOutputStream = new MemoryStream();
		StreamWriter soapMessageWriter= new StreamWriter 
			(appOutputStream);
		soapMessageWriter.Write (xmlVersionString);
		soapMessageWriter.Write (soapEnvelopeBeginString);
		soapMessageWriter.Write (soapBodyBeginString);
		soapMessageWriter.Write (soapBodyString);
		soapMessageWriter.Write (soapBodyEndString);
		soapMessageWriter.Write (soapEnvelopeEndString);
		soapMessageWriter.Flush ();
					
		appOutputStream.Flush();
		appOutputStream.Position= 0;
		StreamReader reader= new StreamReader(appOutputStream);
		StreamWriter writer= new StreamWriter(this.outputStream);
		writer.Write(reader.ReadToEnd());
		writer.Flush();
		appOutputStream.Close();
		break;
	}
	case SoapMessageStage.BeforeDeserialize : 
		break;
	case SoapMessageStage.AfterDeserialize :
		break;
		
}
}

We can see from the above implementation that the data serialized by the ASP.NET plumbing into the memory stream that was returned to it in the ChainStream method is completely ignored and discarded. This is because, we are explicitly packing the XML document as the payload into the SOAP message using the SOAP extension. In light of this modified design, we can now invoke the service method from the client-side application as shown below, merely passing a null without having to create an array of Patient object as in the object-oriented approach.

// Create a Reference to a Web Service Proxy
TransferService intf= new TransferService ();

// Just Pass a Null to the Service Method. The SOAP Extension does the Real Work
intf.transferData (null);

Server-side SOAP Extension

The operations performed by the ChainStream and ProcessMessage methods in the SOAP extension on the server-side of the application are a little different. The Web Service method needs to retrieve the XML document contained within the SOAP message and process it further. Hence, its SOAP extension is used to intervene the message processing on the server-side so as to avoid the deserialization of this XML data into an object hierarchy. The implementation of the ChainStream method saves a reference to the incoming HTTP stream, as shown below:

public class ServerSideSoapExtension : SoapExtension {

private bool incoming= true;
	private Stream httpInputStream;

public override Stream ChainStream (Stream stream) {	
	if (this.incoming) {
		this.httpInputStream = stream;
		this.incoming = false;
	}
	return stream;
	}
}

The SOAP extension on the server-side of our application does its work in the stage that corresponds to SoapMessageStage.BeforeDeserialize. This stage occurs after the ASP.NET plumbing has received the data from the client in the form of an HTTP input stream. In the ProcessMessage method, the SOAP extension reads all the data from the stream that was saved in the ChainStream method and saves it in a string object. This string object represents the entire contents of the SOAP message in XML format. A reference to this string is saved in the request scope state bag provided by the HttpContext.Current.Items collection. The implementation of the Web Service method, namely, transferData, subsequently retrieves this string and performs further processing on the data, as shown below.

public class ServerSideSoapExtension: SoapExtension {
public override void ProcessMessage (SoapMessage message) {

	switch (message.Stage) {		
	case SoapMessageStage.BeforeSerialize : 
		break;
		
	case SoapMessageStage.AfterSerialize :
		break;
	
	case SoapMessageStage.BeforeDeserialize : {

		// Retrieve the SOAP Message from the Input Stream
		// Save the SOAP Message in Request-scope State Bag
		StreamReader soapMessageReader= new StreamReader 
			(this.httpInputStream);
		string soapBodyString= soapMessageReader.ReadToEnd();
		HttpContext.Current.Items["SoapMessage"] = soapBodyString;	
		break;
	}
	case SoapMessageStage.AfterDeserialize :
		break;	
}
}

As we have already retrived the XML document before any deserialization takes place, we can implement the service method, transferData, without any arguments.

public class HL7Parser : System.Web.Services.WebService {
		
[WebMethod]
public object transferData() {
	// Retrieve the XML Document Saved in Request Scope by the 
			SOAP Extension
	string xmlFilepath= (string) 
		HttpContext.Current.Items["SoapMessage"]
	doSomething (soapMessage);
	return null;
}
}

HttpHandlers vs SOAPExtensions

Looking at the implementation of SOAP extension on the server side in the above example, one may wonder if the same result could be accomplished by using a custom HttpHandler to process the HttpRequest in the raw by providing an appropriate implementation of IHttpHandler.ProcessRequest(). While this may accomplish the goal of extracting the XML data from the incoming request stream, there are disadvantages to this approach. Using a custom handler to handle requests sent to the web service would, of course, replace the default handler, namely, System.Web.Services.Protocols.WebServiceHandlerFactory, that processes all requests to *.asmx. This robs the client of the capability to enter the URL of the web service (like, say, http://my.server.com/HL7Parser.asmx) and have ASP.NET on the server side dynamically create an HTML page describing the service's capabilities and methods. Also, if a custom handler other than System.Web.Services.Protocols.WebServiceHandlerFactory is used, then the actual business method of the service, namely, transferData(), will not get automatically invoked after IHttpHandler.ProcessRequest() is completed. Hence, using the SOAP extensions in conjunction with the default HttpHandler for *.asmx, we can make use of the regular plumbing provided by ASP.NET to invoke web services and at the same time manipulate the request/response based on our needs.

Another point to note is that the option of using a custom handler is available only on the server-side. If the client invoking the web service is, say, a Windows Forms application, the only way for it to intervene and modify the message being sent to the server side would be by using SOAP extensions.

Configuring SOAP Extensions

You can configure a SOAP extension to run using a custom attribute that derives from SoapExtensionAttribute or by modifying a configuration file. To use a custom attribute, apply it to each XML Web service method that you want the SOAP extension to run with. When you use a configuration file, the SOAP extension runs with all the XML Web services that are within the scope of the configuration file. The following is an extract from the Web.config file which specifies that ServerSideSoapExtension SOAP extension runs within the relative priority group 0 and has a priority of 1.

<configuration>
    <system.web>
        <webServices>
            <soapExtensionTypes>
               <add type="TransferService.ServerSideSoapExtension, 
					TransferService" priority="1" group="0"/>
            </soapExtensionTypes>
         </webServices>
     </system.web>
</ configuration>

Conclusion

Even though it is often more convenient to work with objects while designing and implementing Web Services, by making use of document/literal style based messaging in conjunction with the powerful features of .NET SoapExtension classes, we are able to deal directly with the XML data rather than a hierarchy of objects. Implementing the Web Services this way facilitates the client and server to interact by “exchanging documents” rather than by “invoking methods”.

Figures

<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions xmlns="http://schemas.xmlsoap.org/wsdl/" 
		xmlns:apachesoap="http://xml.apache.org/xml-soap" 
		xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
		xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
		xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
		xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
		xmlns:tns="http://comframe.com/transfer" 
		xmlns:intf="urn:Transfer" targetNamespace="urn:Transfer">
	<wsdl:types>
		<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
			elementFormDefault="qualified" attributeFormDefault="unqualified" 
			xmlns:tns="http://comframe.com/transfer" 
			targetNamespace="http://comframe.com/transfer">
			<xsd:simpleType name="Age">
				<xsd:restriction base="xsd:unsignedByte">
					<xsd:minInclusive value="0"/>
					<xsd:maxInclusive value="150"/>
				</xsd:restriction>
			</xsd:simpleType>
			<xsd:simpleType name="Gender">
				<xsd:restriction base="xsd:string">
					<xsd:enumeration value="male"/>
					<xsd:enumeration value="female"/>
				</xsd:restriction>
			</xsd:simpleType>
			<xsd:complexType name="Name">
				<xsd:sequence>
					<xsd:element name="first" type="xsd:string"/>
					<xsd:element name="last" type="xsd:string"/>
				</xsd:sequence>
			</xsd:complexType>
			<xsd:complexType name="Patient">
				<xsd:all>
					<xsd:element name="name" type="tns:Name"/>
					<xsd:element name="age" type="tns:Age"/>
					<xsd:element name="gender" type="tns:Gender"/>
				</xsd:all>
			</xsd:complexType>
			<xsd:element name="PatientList">
				<xsd:complexType>
					<xsd:sequence>
						<xsd:element name="patient" type="tns:Patient" 
							maxOccurs="unbounded"/>
					</xsd:sequence>
				</xsd:complexType>
			</xsd:element>
			<xsd:element name="TransferResponse">
				<xsd:complexType/>
			</xsd:element>
		</xsd:schema>
	</wsdl:types>
	<wsdl:message name="exampleSoapIn">
		<wsdl:part name="input" element="tns:PatientList"/>
	</wsdl:message>
	<wsdl:message name="exampleSoapOut">
		<wsdl:part name="output" element="tns:TransferResponse"/>
	</wsdl:message>
	<wsdl:portType name="TransferPort">
		<wsdl:operation name="transferData">
			<wsdl:input name="transferRequest" 
				message="intf:exampleSoapIn"/>
			<wsdl:output name="transferResponse" 
				message="intf:exampleSoapOut"/>
		</wsdl:operation>
	</wsdl:portType>
	<wsdl:binding name="TransferBinding" type="intf:TransferPort">
		<soap:binding style="document" 
			transport="http://schemas.xmlsoap.org/soap/http"/>
		<wsdl:operation name="transferData">
			<soap:operation/>
			<wsdl:input>
				<soap:body use="literal"/>
			</wsdl:input>
			<wsdl:output>
				<soap:body use="literal"/>
			</wsdl:output>
		</wsdl:operation>
	</wsdl:binding>
	<wsdl:service name="TransferService">
		<wsdl:port name="TransferServicePort" 
			binding="intf:TransferBinding">
			<soap:address location="http://localhost/Patient/Transfer"/>
		</wsdl:port>
	</wsdl:service>
</wsdl:definitions>	

Figure 1. WSDL Definitions for TransferService (cont.)

<?xml version="1.0" encoding="utf-8"?>
<PatientData xmlns="http://comframe.com/transcription">
	<patient>
		<name>
			<first>John</first>
			<last>Doe</last>
		</name>
		<age>35</age>
		<gender>male</gender>
	</patient>
	<patient>
		<name>
			<first>Jo</first>
			<last>Patient</last>
		</name>
		<age>30</age>
		<gender>female</gender>
	</patient>
</PatientData>

Figure 2. XML document to be transmitted by the client.

Authors

Dr. Viji Sarathy has worked for several years in the design of object-oriented, distributed software systems. More recently, he is specializing in the design and implementation of software architectures with both .NET and J2EE technologies. He is a MCAD for .NET and Sun Certified Enterprise Architect for J2EE. He is currently a Senior Software Architect at ComFrame Software Corporation. Founded in 1997 in Birmingham, AL, ComFrame delivers a wide range of custom software solutions to its clients. ComFrame has offices in Birmingham, AL, Nashville, TN, and Huntsville, AL. For more information on ComFrame visit www.comframe.com.

News | Blogs | Discussions | Tech talks | White Papers | Downloads | Articles | Media kit | About
All Content Copyright ©2007 TheServerSide Privacy Policy
Site Map