
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.
|
|