3.                        Chapter 3 SOAP: Invoking Web Services

“I think there's a world market for about five computers.” – Thomas Watson.

 

In chapter 1 you learned how to invoke a Web service using the SOAP Toolkit and .NET. In this chapter you will learn how these and other tools use SOAP messages to invoke Web services. I will explain the goals and architecture of SOAP and the ways it can be used including messaging and RPC. This chapter’s objective is to teach you what SOAP is capable of doing and how, so that you get a better understanding of the tools you’ll be using such as .NET and the SOAP Toolkit. Such understanding will come in handy when you need to invoke a Web service and you find the tools have limitations that prevent from using them. This and the next chapter are tightly integrated and together complete the picture of how Web services work.

 

What Is SOAP

Since 1994, the Web has been growing tremendously and has become the Internet’s killer application. The Internet itself provides basic network connectivity between millions of computers using TCP and IP as shown in Figure 1. This connectivity is not of much value unless applications running on different machines decide to communicate with one another leveraging the unerlying network. Traditionally, each type of application has invented and used its own application-level protocol which sits on top of TCP. For example, HTTP is an application-level protocol desgined for use between the Web browser and Web server as shown in Figure 3-1. The arrows in figure 3-1 show the logical communication that goes on between peer layers on different hosts. The actual information flow goes down the stack on one host then up the stack on the other host.

 

 

Figure 3‑1

The Internet protocol stack used for browser/server communication

 

Despite HTTP’s huge success as the Internet’s killer application protocol, it is limited to fairly simple commands centered around requesting and sending resources, e.g. GET, POST and PUT. The result is that today we have millions of interconnected computers that leverage the Internet primarily for browsing the Web but cannot, despite of the connectivity, freely exchange data between applications. SOAP[1] proposes to solve this problem by defining a standard protocol that any application can use to communicate and exchange data with any other application. Figure 3-2 shows how SOAP can be used over TCP/IP leveraging the current Internet infrastructure.

 

Figure 3‑2

SOAP enables application-to-application communication over any transport protocol including TCP

 

SOAP is an application-level protocol so it can work directly over a transport protocol such as TCP. However, today’s Internet infrastructure is riddled with proxies and firewalls that typically allow only HTTP traffic. In order for all Internet-connected applications to communicate, SOAP must be able to flow over the current Internet infrastructure including firewalls and proxies. To achieve this SOAP can be layered over HTTP as shown in figure 3-3.

 

Figure 3‑3

SOAP can be used over HTTP to enable application-to-application communications over existing Internet infrastructure with its firewalls and proxies

 

Layering SOAP over HTTP means that a SOAP message is sent as part of an HTTP request or response which makes it easy to communicate over any network that permits HTTP traffic. HTTP is also a good choice because, just like Web browsers, it is pervasive on all computing platforms and devices.

To achieve platform independence and maximum interoperability, SOAP uses XML to represent messages exchanged between the client and the Web service. Like HTTP, XML is also pervasive and you can find an XML parser for nearly any computing platform (or you can write your own if need be). By leveraging HTTP and XML, SOAP provides application to application communications between applications running on any platform and connected over the existing Internet infrastructure.

 

SOAP Architecture

The key architectural aspect of SOAP is its simple design which is intended to encourage vendors to adopt and implement the protocol. SOAP doesn’t try to solve all the problems of distributed applications communication, it focuses instead on the minimum standards required to send messages from one application to another. For example, SOAP does not include specifications for security or distributed transactions, both of which are commonly needed for distributed applications. However, the SOAP architecture is flexible and extensible through the use of SOAP headers which I’ll discuss later in this chapter.

SOAP’s architecture is centered on sending a SOAP message from the sender (the client) to the ultimate destination (the Web service) with optional intermediate nodes between the two as shown in Figure 3-4. Note that there’s nothing in this architecture that talks about accessing objects and invoking methods remotely as in a Remote Procedure Call. Instead, SOAP focuses on sending a message from a sender to a recipient. To fully understand SOAP, you need to think in terms of messages exchanged between client and Web service. Using SOAP for RPC is just a special application of messaging that combines two messages in opposite directions.

Figure 3‑4

A SOAP message travels from the sender to the ultimate destination with optional intermediate nodes between the two

.

The SOAP Message

The SOAP message architecture consists of an Envelope which contains an optional Header and a mandatory Body as shown in figure 3-5. The Body itself contains the payload (the data being sent) and/or optional error information.

Figure 3‑5

SOAP message architecture

A basic SOAP message is a well-formed XML document consisting of an <Envelope> and <Body> elements that belong to the SOAP envelope namespace defined as http://schemas.xmlsoap.org/soap/envelope/. Listing 3-1 shows an example SOAP message with the soapenv prefix declared for the SOAP namespace. For the rest of this chapter, I’ll refer to SOAP elements using soapenv as the namespace prefix, e.g. <soapenv:Envelope> and <soapenv:Body>.

 

Listing 3‑1 An example SOAP message.

<soapenv:Envelope

xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

  <soapenv:Body>

    <SubmitInvoice

      xmlns="http://learnxmlws.com/nwind.net">

      <invoiceDoc>...</invoiceDoc>

    </SubmitInvoice>

  </soapenv:Body>

</soapenv:Envelope>

 

 

The <soapenv:Envelope> is the document element (i.e. top level element) of the SOAP message and usually has several namespace declarations including the SOAP envelope namespace. Within the <soapenv:Envelope>, there’s always one <soapenv:Body> element which contains the payload of data being sent. Looking at Listing 3-1, you see that the payload is <SubmitInvoice> and its contents.

In addition to <soapenv:Body>, <soapenv:Envelope> may also contain an optional <soapenv:Header> element which is used to send information in addition to what’s already in the <soapenv:Body> payload. Listing 3-2 shows the same SOAP message as in Listing 3-1 with a <soapenv:Header> added.

 

Listing 3‑2 A SOAP message with headers.

<soapenv:Envelope

xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

  <soapenv:Header>

 <authHeader

   xmlns="http://learnxmlws.com/nwind.net">

    <authToken>

      4fthlE3GpY2cnrSvylSzYtV3HjL8ovFHm91P

    </authToken>

  </authHeader>

  </soapenv:Header>

  <soapenv:Body>

    <SubmitInvoice

      xmlns="http://learnxmlws.com/nwind.net">

      <invoiceDoc></invoiceDoc>

    </SubmitInvoice>

  </soapenv:Body>

</soapenv:Envelope>

 

In this example, the <soapenv:Header> is used to send an application-specific authentication header represented by the element <authHeader>. I’ll discuss SOAP headers in more detail later in this chapter.

The fourth and last SOAP element is <soapenv:Fault> which is used to communicate error information. If used, the <soapenv:Fault> appears as a child of the <soapenv:Body> and contains several child elements such as <faultcode> and <faultstring>. I’ll explain the contents of <soapenv:Fault> and how to use it to communicate error information later in this chapter.

SOAP Message Formats

SOAP messages can have several different formats which differ in how the data inside <soapenv:Body> and potentially inside <soapenv:Header> is formatted.

To send data in a SOAP message, you must first serialize this data in a format that can be understood by the message recipient. For example, in chapter 2 you learned how to use the System.Xml.Serialization namespace to easily serialize objects to an XML document. To send this XML document in a SOAP message, you can simply create the SOAP message and include this document within the <soapenv:Body>[2]. In this case, the message you end up with is said to follow document-style SOAP and have a literal payload and is usually referred to as a document/literal message. Document and RPC are two styles of SOAP messages both defined in the SOAP specification. Here’s what each term means in a nutshell:

·         Document: <soapenv:Body> contains one or more child elements called parts. There are no SOAP rules for what the <soapenv:Body> contains, it can contain whatever the  sender and the receiver agree upon.

·         RPC: Here the <soapenv:Body> contains the name of the method or remote procedure you are invoking and an element for each parameter of that procedure. Section 7 of the SOAP specification defines exactly what the <soapenv:Body> contains when using RPC-style SOAP.

In addition to the two SOAP message styles, there are two formats for serializing data into XML. The format you choose determines how your data is serialized into the <soapenv:Body> and <soapenv:Header> elements. Here’s a definition of each serialization format.

·         Encoded: Data is serialized according to some encoding rules. The most common are the encoding rules specified in Section 5 of the SOAP specification which define how objects, structures, arrays, and object graphs should be serialized. With Section 5 encoding, the client and the service deal with data in terms of objects and structures.

·         Literal: Data is serialized according to an XML Schema, usually XSD. There are no special encoding rules that dictate how to serialize the data and the serialization format need to convey the fact that the data came from an array or an object etc. With literal format, the client and service deal with the data in terms of XML documents rather than objects and structures.

Figure 3-6 shows the <soapenv:Body> element and its contents with the labels indicating the effect of document, RPC, literal and encoded.

 

Figure 3‑6 How RPC/document and literal/encoded affect the SOAP <Body> element and its content

Theoretically, your choice of using document or RPC is independent of your choice of encoded or literal, giving you a total of four different combinations, namely document/literal, document/encoded, RPC/literal, and RPC/encoded.

Practically however, all implementations tend to combine RPC with encoded and document with literal. This makes sense when you group SOAP applications into two categories: Those exchanging business documents, such as business-to-business applications, tend to use document/literal SOAP messages. While applications using SOAP as an RPC protocol to invoke remote objects (instead of DCOM for example) tend to use RPC/encoded SOAP messages.

When SOAP 1.1 came out, most people saw SOAP as a simple RPC protocol that can easily work over the Internet with its firewalls and proxies. As a result, there was a rush of SOAP implementations mostly focused on RPC and using Section 5 encoded messages. For example, the Microsoft SOAP Toolkit, which is designed to let you expose COM components as Web services, uses RPC/encoded SOAP messages by default. This makes sense because the objective is to remotely invoke a method on your COM component which means you are thinking in terms of RPC. When using .NET you can expose Web services in two ways: Remoting and .NET Web services. Remoting uses RPC/encoded SOAP while .NET Web services, designed for document exchange via messaging, use document/literal SOAP by default. This stresses the appropriate usage of RPC/encoded vs. document/literal SOAP messages.

Document/literal is the better fit for SOAP messaging because you have full control over the format of the message payload. If you are building a business-to-business application, you might be exchanging documents, such as invoices and purchase orders, whose schemas are predefined by some standards body. By using document/literal, you can control the exact message format and make it conform to this standard schema. Going forward, the only good reason to use RPC/encoded SOAP is when you are exposing server-side objects to be invoked by remote clients that are designed to use RPC. If the server-side objects are COM-based, you would use the SOAP Toolkit, if they are .NET objects, you would use .NET Remoting. In the next two sections I will show you how to use SOAP for messaging and RPC along with practical applications of each.

 

Messaging with SOAP

SOAP messaging is actually very easy to implement. All you need is to format your request as an XML document and insert it within the <soapenv:Body> of a document/literal message. How you create your request as XML depends on the nature of the request and where the data is coming from. If your application uses .NET objects internally to represent the request data, you can easily create the XML by using .NET Serialization as explained in chapter 2. Alternatively, if the request data is coming from SQL Server, you can get the data out as XML using SQL Server’s FOR XML clause or one of the many other XML features of SQL Server.

As an example of messaging, I built a simple Web service to receive invoice documents, validate them, save them in a SQL Server database and return a receipt id. Listing 3-3 shows an example SOAP request and the corresponding response document for this particular Web service.

 

Listing 3‑3 Document/literal request and response messages.

<!-- Request document -->

<soapenv:Envelope

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2001/XMLSchema"

xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

<soapenv:Body>

<invoice xmlns="http://schemas.learnxmlws.com/invoice">

<invoiceNumber>123-YJK-9087</invoiceNumber>

<supplierID>5</supplierID>

<invoiceDate>2001-08-11</invoiceDate>

<poNumber>PO-0983</poNumber>

<subTotal>21.90</subTotal>

<salesTax>9.86</salesTax>

<paymentReceived>0</paymentReceived>

<amtDue>31.76</amtDue>

<terms>net 30</terms>

<contactName>Yasser Shohoud</contactName>

<contactNumber>(703) 626-6822</contactNumber>

<promotion></promotion>

<invoiceItems>

    <item>

       <partNum>1234-KUY</partNum>

       <quant>2</quant>

       <unitPrice>10.95</unitPrice>

       <total>21.90</total>

    </item>

</invoiceItems>

</invoice>

</soapenv:Body>

</soapenv:Envelope>

 

 

<!-- Response document -->

<soapenv:Envelope

xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'

xmlns:xsd='http://www.w3.org/2001/XMLSchema'

xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>

<soapenv:Body>

    <Receipt xmlns='http://schemas.learnxmlws.com/invoice'>9</Receipt>

</soapenv:Body>

</soapenv:Envelope>

 

 

Both request and response documents are examples of document/literal SOAP messages. The request document begins with the <soapenv:Envelope> element which contains <soapenv:Body> which in turn contains the invoice document. In this example, the invoice document is the request message’s payload. The entire invoice document, beginning with <invoice> all belongs to the namespace http://schemas.learnxmlws.com/invoice which is the targetNamespace defined in the invoice’s schema document invoice.xsd.

Following the same structure, the response message’s payload contains a simple document with one elmenet: <Receipt>, which contains the receipt number confirming that the invoice has been successfully processed.

SOAP Over HTTP

While SOAP can be used over a variety of transports, HTTP is the most commonly used because of its ubiquity. HTTP requests are very easy to form, they consist of simply an HTTP command such as POST or GET followed by the URL you’re requesting and the protocol version e.g. HTTP 1.0. An HTTP request or response usually contains HTTP headers which are simple information organized in name-value pairs. For example, this is an HTTP GET request for the resource /images/pp_header_02.jpg with the two HTTP headers Accept and Referer (yes Referer has a typo, it’s that way in the HTTP standard):

 

GET /images/pp_header_02.jpg HTTP/1.1

Accept: */*

Referer: http://localhost/Logon.asp?url=/Default.asp

 

When using SOAP over HTTP, there are two rules to remember. First, you must set the Content-Type header to text/xml telling the recipient that the HTTP request or response contains an XML document, i.e. a SOAP message. Second, when sending a SOAP request, you must set the HTTP SOAPAction header. This special header always contains a quoted value and is designed to convey extra information about the SOAP message at the HTTP level. This can be useful for example for firewalls that filter SOAP/HTTP traffic based on certain values of the HTTP SOAPAction header. There are no rules for the value of SOAPAction, you have to set it to whatever the Web service expects it to be. This lack of definite rules has caused much controversy over the use of SOAPAction and whether it’s needed at all. Practically, you should not design your Web services to rely on SOAPAction unless you absolutely need to. Listing 3-4 shows an example HTTP request with the above SOAP request message and the SOAPAction and Content-Type headers.

 

Listing 3‑4 A SOAP message sent over HTTP.

POST /vbwsbook/chapter3/MessagingWS/invoiceMessaging.asp HTTP/1.0

SOAPAction: "urn:InvoiceAction"

Content-Type: text/xml

Accept-Language: en-us

Content-Length: 929

Accept: */*

User-Agent: Mozilla/4.0 (compatible; Win32; WinHttp.WinHttpRequest.5)

Host: localhost

Connection: Keep-Alive

 

<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

...

</soapenv:Envelope>

 

Capturing SOAP Messages

Throughout this chapter and indeed the rest of the book, you’ll need a way to capture SOAP messages exchanged between client and service for learning and troubleshooting purposes. There are a few free tools that allow you to do this easily. One of those tools, Trace Utility, comes with the Microsoft SOAP Toolkit and can be used to intercept and record SOAP messages between the client and server. The tool works by inserting itself between the client and the Web server where your Web service is running. Basically, it listens on a port that you specify, and forwards all traffic it receives to a Web server and port that you also specify. Figure 3-7 shows an example of configuring it to listen on port 8080 and forward all traffic to the local server on port 80.

 

 

Figure 3‑7 Configuring the Trace Utility

 

Once you configure the Trace Utility to listen to a port such as 8080, you must make your client send all requests to this port on the localhost. Therefore, instead of sending a SOAP request to http://hostname/myservice.asp , you’d send it to http://localhost:8080/myservice.asp. If you want to capture SOAP messages while running code in this chapter, simply run the Trace Utility and make it listen on port 8080 and replace the service URL in the code to point to port 8080 on the localhost. The Trace Utility will intercept and capture the request and response messages and display them in the trace window.

 

The Web Service

To show you how easy it is to create and process the messages in Listing 3-3, I created the example Web service in classic ASP using only the Microsoft XML parser MSXML version 4.0 as shown in Listing 3-5. 

 

Listing 3‑5 A classic ASP page that acts as a SOAP endpoint. (VBWSServer\VBWSBook\Chapter3\MessagingWS\invoiceMessaging.asp).

<%@Language="VBScript" %>

<%

Const adVarChar=200

Const adInteger=3

Const adParamInput=1

Const adParamOutput=2

Const adCmdStoredProc=4

Dim CONNSTR

Dim NS

Dim XSD_LOC

Dim requestDoc

Dim doc

Dim schemaCache

Dim MY_ACTION

 

CONNSTR="provider=SQLOLEDB;server=(local);database=WSBook;uid=sa;pwd=;"

NS="http://schemas.learnxmlws.com/invoice"

'Check the SOAPAction

Set requestDoc=Server.CreateObject("MSXML2.DOMDocument")

'Load the SOAP message

requestDoc.async=false

If Not requestDoc.Load(Request) Then

Call SendFault("soap:Client", _

    "Error loading request doc", _

    "Message processor", _

    requestDoc.parseError.reason & _

    " at line " & _

    requestDoc.parseError.line, _

    requestDoc.parseError.errorCode)

Response.End

Else

Call requestDoc.setProperty("SelectionNamespaces", "xmlns:vbws='" & NS & "'")

End If

 

XSD_LOC=Server.MapPath("./invoice.xsd")

Set doc=Server.CreateObject("MSXML2.DOMDocument")

Set schemaCache = Server.CreateObject("MSXML2.XMLSchemaCache")

On Error Resume Next

'Add the schema location and target namespace

Call schemaCache.Add(NS,XSD_LOC)

If Err.Number <> 0 Then

    Call SendFault("vbws:Application","Failed to add schema",Err.Source,Err.Description,Err.number)

    Response.End

End If

'get the invoice document out of the

'SOAP message

Set doc.schemas = schemaCache

If Not doc.loadxml( _

requestDoc.selectSingleNode("//vbws:invoice").xml) Then

 

Call SendFault("soap:Client","Error validating invoice doc", _

    "Document validation", _

    doc.parseError.reason & " at line " & doc.parseError.line, _

    doc.parseError.errorCode)

        Response.End

Else

 

 

    Dim invNum

    Dim RegId

    Dim supplier

    Call doc.setProperty("SelectionNamespaces", "xmlns:vbws='" & NS & "'")

    'get invoice number and supplier id

    invNum=doc.selectSingleNode("//vbws:invoiceNumber").text

    supplier=doc.selectSingleNode("//vbws:supplierID").text

 

    Dim cmd

    Set cmd=Server.CreateObject("ADODB.Command")

    cmd.ActiveConnection=ConnStr

    If Err.Number <> 0 Then

Call SendFault("soap:Server","Error connecting to database", _

    "Importing invoice", _

                 Err.Description,Err.Number)

        Response.End

    End If

    Dim param

    'call the stored procedure

    'to register the invoice

    Set param=cmd.CreateParameter( _

       "invoiceNumber",adVarChar, _

       adParamInput,50,invNum)

    cmd.Parameters.Append(param)

Set param=cmd.CreateParameter("supplierId",adInteger, _

       adParamInput,0,supplier)

    cmd.Parameters.Append(param)

Set param=cmd.CreateParameter("RegId",adInteger, _

       adParamOutput)

    cmd.Parameters.Append(param)   

    cmd.CommandText="AddIncomingInvoice"

    cmd.CommandType=adCmdStoredProc

cmd.Execute

    If Err.number <> 0 Then

    Response.Write Err.Description

    Response.End

    End If

    RegId=cmd.Parameters("RegId").Value

    cmd.ActiveConnection.Close

    Set cmd=Nothing

    'save the invoice document to disk

    Call doc.Save( _

    Server.MapPath("./" & supplier & "-" & invNum & ".xml"))

    Set doc=Nothing

    'write the response

WriteSOAPResponse("<Receipt xmlns='" & NS & "'>" & RegId & "</Receipt>")

End If

 

Sub SendFault(faultCode,faultString,source,descr,num)

Dim faultMsg

faultMsg = "<soap:Fault xmlns:vbws='" & NS & "'><faultCode>" & faultCode & "</faultCode>" & _

          "<faultString>" & faultString & "</faultString>" & _

          "<detail><vbws:ErrNumber>" & num & "</vbws:ErrNumber>" & _

          "<vbws:ErrDescr>" & descr & "</vbws:ErrDescr>" & _

          "<vbws:ErrSource>" & source & "</vbws:ErrSource>" & _

          "</detail></soap:Fault>"

Call WriteSOAPResponse(faultMsg)              

End Sub

 

Sub WriteSOAPResponse(msg)

    Response.ContentType="text/xml"

Response.Write "<soap:Envelope xmlns:xsi=" & _

       "'http://www.w3.org/2001/XMLSchema-instance'" & _

        " xmlns:xsd='http://www.w3.org/2001/XMLSchema'" & _

        " xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'><soap:Body>"

Response.Write msg

Response.Write "</soap:Body></soap:Envelope>"

End Sub

 

%>

 

I chose an ASP page instead of a COM component because it can easily receive an HTTP request and it’s very easy to deploy by itself: Just copy it to the server where you want to deploy it. Of course you will need to modify the database connection string to point to the book’s database (see the CD readme file for more information on setting up the database).

First, I create an XML DOM Document and load the request message into it. I then set the document’s SelectionNamespaces property mapping the vbws prefix to the invoice namespace. I need to do this because I will use XPath to retrieve elements from the document and I must use the fully qualified element names. I then create an XMLSchemaCache object and add to it the invoice XSD schema and the invoice namespace to validate the incoming invoice document as shown in Chapter 2. Next, I pull out the incoming invoice document using requestDoc.selectSingleNode("//vbws:invoice").xml and load this in a new DOMDocument. At this point, the invoice is validated against the XSD schema so I catch any errors and send them back using Response.Write. Later in this chapter, I will add code to send error information back to the client using <soapenv:Fault>.

After the invoice is validated, I invoke a stored procedure called AddIncomingInvoice passing it the supplier id and the invoice number from the incoming invoice document. The stored procedure registers the invoice and returns a receipt number. I then save the invoice document to disk in the same folder where this ASP page exists. Finally, I send back a SOAP response with one element, <Receipt>, which contains the receipt number returned by the stored procedure.

The Client

On the client side, I wrote a utility called SOAPSender which lets you easily create and send SOAP messages. The utility is available in two versions, one written in VB 6 as a COM component and uses only MSXML 4.0, the other written in VB .NET and uses classes from the .NET framework including System.Xml and System.Net. I then wrote a Windows client using VB 6 and SOAPSender as shown in Listing 3-6.

 

Listing 3‑6 A client submitting an invoice to the Web service. (VBWSClientCode\Chapter3\SOAPSender\Form1.frm).

Private Const SERVICE_URL = _

"http://VBWSServer/vbwsbook/chapter3/MessagingWS/InvoiceMessaging.asp"

 

Private Sub cmdSendInvoice_Click()

Dim doc As MSXML2.DOMDocument40

Set doc = New MSXML2.DOMDocument40

If Not doc.Load(txtInvoice.Text) Then

    MsgBox "Error loading XML document " & vbCrLf & _

        doc.parseError.reason, vbCritical

    Exit Sub

End If

Dim vbws As VBWSSoap.SOAPSender

Set vbws = New VBWSSoap.SOAPSender

On Error GoTo eh

vbws.ServiceUrl = SERVICE_URL

vbws.SoapAction = """"""

vbws.WriteBodyXml doc.documentElement.xml

vbws.send

If Not doc.loadXML(vbws.GetResponseString) Then

    MsgBox "Error loading response document " & vbCrLf & _

        doc.parseError.reason & vbCrLf & "Response document is" & vbCrLf & _

        vbws.GetResponseString(), vbCritical

    Exit Sub

Else

    MsgBox "Response is " & vbws.GetResponseString

End If

 

Exit Sub

eh:

    MsgBox "Error sending invoice document" & vbCrLf & Err.Description

End Sub

 

To send the invoice document, I first load it into a DOMDocument called doc using doc.Load. I then create a SOAPSender object and set its ServiceUrl property to point to the URL of my ASP page. I also set its SoapAction property to a pair of double quotes (the equivalent of an empty SOAPAction) since this Web service does not rely on SOAPAction.

I then call its WriteBodyXml method passing it the invoice document’s XML. WriteBodyXml simply takes the XML you give it and stuffs it as the content of the <soapenv:Body> element. To send the invoice document, I simply call Send then retrieve the response using SOAPSender’s GetResponseString method which returns the entire response as a string. Because this example uses messaging, the client is not designed as if invoking a method on a remote object, instead, it is sending a document to a remote recipient. This is the key distinction between messaging and RPC.

As you can see, once you know some SOAP fundamentals, the client and service code are straightforward and do not require a special SOAP tool. This is especially true for document/literal SOAP messages as there message payload can be any XML document. If you’re building data exchange or business-to-business integration Web services, you should design them using document/literal. You can then implement them using any technology or toolset you like including rolling your own like I showed you here.

 

RPC with SOAP

When you have an existing distributed application with client and server pieces using DCOM to communicate between them, it’s usually impractical to re-architect and re-write the application using XML messaging. At a minimum, replacing DCOM with messaging requires major changes and additions to the server to receive and process incoming XML documents and send responses as outgoing XML documents. Additionally, the client must be re-written to format and send requests as XML documents and receive and parse responses. These changes are small compared to the major change in programming model that developers must learn and adopt. Developers writing the client no longer instantiate a server object and call its methods passing them parameters and getting back a return value, instead they must somehow serialize all data to XML to send the request, then deserialize data from XML upon receiving the response. Finally, if the existing application architecture does not lend itself to messaging, by using stateful server objects for example, then a major re-architecture and/or some custom SOAP extensions are needed to replace DCOM with SOAP messaging.

To ease converting existing applications from DCOM to SOAP, you can write a layer on the client side that acts as the Web service proxy. As a proxy, this layer would give the client the impression that it’s calling methods on a server object. The proxy would be responsible for serializing the parameters to XML, sending them as parts of a SOAP message, then parsing and deserializing the response message and returning data to the client in native data types such as strings, integers, and arrays. On the server side, you would write another layer that is responsible for receiving SOAP messages, deserializing their content to native data types, instantiating the server COM component and invoking the appropriate method, then serializing the return value and sending back a SOAP response. The proxy layer needs to communicate specific information about the RPC call to the server, such as the intended object, a method name, the name of each parameter, and of course the value or content of each parameter. Similarly, the server needs to communicate back the return value and the output value of all in/out (ByRef) parameters. If you write both of these layers, you can communicate this information in any format you like as long as both layers agree on the format. However, if you are exposing the server side as a public Web service and many clients will have to communicate with it, then all clients and the service must use a common format for communicating this information. Section 7 of the SOAP specification defines a standard format for communicating this information when using SOAP for RPC. The SOAP specification also defines a standard way of serializing/deserializing native application types such as strings, arrays, and structures, which is especially important to enable interoperation among clients and servers. This standard serialization format is commonly called Section 5 encoding after the SOAP specification section where it is defined (I’ll explain SOAP Section 5 encoding later in this chapter). According to the SOAP specification, you can use SOAP for RPC (a.k.a Section 7) without necessarily using the standard Section 5 encoding, i.e. you can serialize your data in any format you like while still using SOAP for RPC. Practically however, it makes sense to use Section 5 encoding format when using RPC, especially when you care about interoperability, because most SOAP stacks and tools, such as the Microsoft SOAP Toolkit, the .NET Framework, and many others combine RPC with Section 5 encoding. 

In this section I will show you an example of using SOAP for RPC with a VB 6 client that uses data binding to show a list of orders in a data grid for the selected employee (see Figure 3-8). The grid is bound to a Recordset returned from invoking a remote COM component using DCOM. I’ve replaced DCOM with SOAP and implemented a simple proxy as a VB 6 class with its DataSourceBehavior set to vbDataSource so that the data grid can bind to an instance of this proxy class.

 

Figure 3‑8 An example Visual Basic client using data binding to display employee orders

 

On the server side, I added a stub layer written as a simple ASP page that receives the SOAP message, invokes the COM component then serializes and returns the Recordset. The server COM component did not have to change at all. The client had to change to bind to the proxy class instead of directly to a Recordset, this change required exactly 3 lines of code. The existing server-side COM component is called RecordsetExample and exposes a method called GetEmpOrders which takes in an employee id and queries the Northwind Orders table for all orders with the given employee id and returns the result in an ADO Recordset:

Public Function GetEmpOrders(ByVal EmpId As Long) As Recordset

To expose the COM component as a Web service, I wrote the ASP page shown in Listing 3-7 which uses only MSXML 4.0 and ADO 2.6.

 

Listing 3‑7 An ASP page to expose an existing COM component as a Web service. (VBWSBook\Chapter3\EmployeeOrders\EmpOrders.asp).

<%@Language="VBScript" %>

<%

CONST adPersistXML=1

 

Dim NS

Dim requestDoc

Dim EmpId

Dim RpcEx

Dim rs

 

On Error Resume Next

 

 

NS="http://schemas.learnxmlws.com/employeeOrders"

Set requestDoc=Server.CreateObject("MSXML2.DOMDocument")

'Load the SOAP message

requestDoc.async=false

If Not requestDoc.Load(Request) Then

Response.Write "Error loading request doc " & Err.Description

Response.End

Else

Call requestDoc.setProperty("SelectionNamespaces", _

       "xmlns:vbws='" & NS & "'")

End If

 

'get the employee id

EmpId=requestDoc.selectSingleNode("//vbws:GetEmpOrders/EmpId").text

If Err.number <> 0 Then

Response.Write "Error retrieving employee id: " & _

       Err.Description

End If

 

Set RpcEx =Server.CreateObject("RPCExamples.RecordsetExample")

Set rs=RpcEx.GetEmpOrders(EmpId)

If Err.number <> 0 Then

Response.Write "Error retrieving data: " & Err.Description

End If

'write the response

 WriteSOAPResponse("<vbws:GetEmpOrdersResponse xmlns:vbws='" & _

       NS & "'" _

"soapenv:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>" & _

"<theReturn>" & Rs2Xml(rs) & _

"</theReturn></vbws:GetEmpOrdersResponse>")

If Err.number <> 0 Then

Response.Write "Error serializing data: " & Err.Description

End If

Function Rs2Xml(rs)

    Dim doc

    Set doc = Server.CreateObject("MSXML2.DOMDocument")

    rs.Save doc, adPersistXML  

    Rs2Xml = "<![CDATA[" & doc.xml & "]]>"

End Function

Sub WriteSOAPResponse(msg)

    Response.ContentType="text/xml"

    Response.CharSet="iso-8859-1"

Response.Write "<soapenv:Envelope " & _

       "xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" & _

       " xmlns:xsd='http://www.w3.org/2001/XMLSchema'" & _

       " xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>" & _

       "<soapenv:Body>"

Response.Write msg

Response.Write "</soapenv:Body></soapenv:Envelope>"

End Sub

%>

I start by loading the request message in a DOMDocument named requestDoc. I then retrieve the employee id using XPath to select the element called EmpId. I then instantiate an object from the example component RPCExamples.RecordsetExample and call GetEmpOrders passing it the employee id to get back an ADO Recordset. I then call WriteSOAPResponse passing it the contents of <soapenv:Body> as in the example response message in listing 3-6. To serialize the Recordset, I wrote a function called Rs2Xml which creates a DOMDocument and passes it to rs.Save then wraps the output XML in a CDATA section. Obviously, all the serialization heavy lifting is done by the Recordset itself. My legacy COM component is now exposed as a Web service and ready to be invoked via SOAP RPC/encoded messages. Listing 3-8 shows the corresponding request and response SOAP RPC/Section 5 encoded messages.

 

Listing 3‑8 Example request message passing an employee id and the response message containing an ADO Recordset with employee orders

POST /vbwsbook/chapter3/EmployeeOrders/EmpOrders.asp HTTP/1.0

SOAPAction: ""

Content-Type: text/xml

Accept-Language: en-us

Content-Length: 351

Accept: */*

User-Agent: Mozilla/4.0 (compatible; Win32; WinHttp.WinHttpRequest.5)

Host: VBWSServer

Connection: Keep-Alive

 

<soapenv:Envelope

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xmlns:xsd="http://www.w3.org/2001/XMLSchema"

xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

<soapenv:Body>

<vbws:GetEmpOrders

    xmlns:vbws="http://schemas.learnxmlws.com/employeeOrders"

    soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">

<EmpId xsi:type="xsd:int">3</EmpId>

</vbws:GetEmpOrders>

</soapenv:Body>

</soapenv:Envelope>

 

 

HTTP/1.1 200 OK

Server: Microsoft-IIS/5.1

Date: Mon, 03 Sep 2001 13:52:26 GMT

Connection: keep-alive

Connection: Keep-Alive

Content-Length: 49198

Content-Type: text/xml; Charset=iso-8859-1

Cache-control: private

<soapenv:Envelope

xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'

xmlns:xsd='http://www.w3.org/2001/XMLSchema'

xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>

<soapenv:Body>

<vbws:GetEmpOrdersResponse

xmlns:vbws='http://schemas.learnxmlws.com/employeeOrders'

soapenv:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>

<theReturn xsi:type='xsd:string'>

<![CDATA[

<xml

xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882"

xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"

xmlns:rs="urn:schemas-microsoft-com:rowset"

xmlns:z="#RowsetSchema">

...

</xml>

]]>

</theReturn>

</vbws:GetEmpOrdersResponse>

</soapenv:Body>

</soapenv:Envelope>

 

Looking at listing 3-8, you’ll see that the request message uses HTTP POST with SOAPAction "" because the Web service does not rely on SOAPAction. Within the request message’s <soapBody>, there’s a single element that must have the same name as the method, i.e. GetEmpOrders. This element’s namespace is called the operation namespace and is determined by the creator of the Web service, which in this example, I decided to make http://schemas.learnxmlws.com/employeeOrders. The soapenv:encodingStyle attribute indicates the type of encoding used for the method parameters, with the value http://schemas.xmlsoap.org/soap/encoding/ indicating SOAP Section 5 encoding.<GetEmpOrders> must contain an element for each parameter of the method being invoked with the same name as the parameter, EmpId in this case. Note that according to SOAP Section 5 rules, these elements must not belong to any namespace, so <EmpId> has no namespace prefix or default namespace declaration. EmpId is a VB Long which maps to an XSD int and, when serialized according to Section 5 encoding rules, comes out as the string “3”. Although this is simple and quite intuitive, it is important to standardize to enable interoperability among different SOAP implementations. Otherwise, someone might decide to serialize the integer value 3 as 0x03 which would be incompatible with my Web service. Note that xsi:type="xsd:int" is used to indicate the type of <EmpId> which might be helpful for the server when it tries to deserialize this parameter. xsi:type is not the only way to specify parameter types nor is it required. You will learn in Chapter 4 how an XSD schema as part of a WSDL document serves the same purpose and can be used instead of inline type information.

Still on listing 3-8, the response begins with HTTP status code 200 which indicates that the RPC call completed successfully. Within <soapenv:Body> there’s one element which, only by convention, is named after the method name with "Response" appended to it, i.e. <GetEmpOrdersResponse>. The method’s return value is repesented by an element that must be the first child of <GetEmpOrdersResponse>. The name of the return value element, e.g. <theReturn> is insignificant. In this example, the method call returns an ADO Recordset so I use its Save method to serialize it to XML. I cannot however just stuff the serialized Recordset into <theReturn> because the result would be invalid according to Section 5 encoding rules. Instead, I enclose it in a CDATA section making it appear as if it were just a string with no XML markup, hence as far as SOAP is concerned, the return value’s type is simply a string, i.e. xsi:type="xsd:string". On the client side, the proxy will extract this string, loaded in a DOMDocument, then deserialize or rehydrate an ADO Recordset from it and return it to the client application which doesn’t know how this Recordset was created or where it came from.

Listing 3-9 shows the proxy class used by the client to invoke th