How to Call a .NET-based Web Service Using the SOAP::Lite Perl Library

 

Alfred Broderick
Microsoft Corporation

December 2002

Applies to:
   Web Services Interoperability
   Microsoft .NET
   Microsoft SQL Server 2000
   SOAP::Lite for Perl

Summary: Describes how to call a .NET-based Web service from a UNIX-based system and how to return a Microsoft .NET DataSet to a Perl array using SOAP::Lite. (21 printed pages)

Contents

Introduction
Connecting to the .NET-based Web Service
   HelloWorld() – A Method That Takes Zero Parameters
   SOAPAction
   Add() – A Method That Takes Two Parameters
   XML Soap Response
   SOAP Headers
   Maptype
Handling the Results
   .NET WebService Studio
   BASIC Authentication
   Handling Errors
   Accessing Complex Data with XPath
   .NET DataSets
   Custom Deserializer
Conclusion
   For More Information

Introduction

On a recent consulting engagement I was asked to create a simple method for Perl users on Solaris to access a database running on Microsoft® SQL Server™ 2000. My customers converted their database to Microsoft SQL Server 2000, but they had a large library of Perl scripts that contained business logic they were not ready to convert. Their goal was to continue to use these Perl scripts with the new Microsoft SQL Server 2000 database with a minimum of changes. SOAP::Lite for Perl looked like a good solution for this situation.

This article describes how I used Perl and SOAP::Lite on Solaris to talk to and retrieve SQL Server data through a Microsoft® .NET-based Web service on Microsoft® Windows® 2000, and discusses the process I followed from beginning to end.

The examples range from very simple Web services that return only strings to those that return results from Microsoft SQL Server queries. I show you the Web service code written in C# and the client code written in Perl with SOAP::Lite, how to send and receive data in SOAP headers, and how to handle errors with SOAP faults.

Note  All the examples assume that you have Perl, SOAP::Lite, and all necessary prerequisite packages installed. If you don't have access to a UNIX box and you want to experiment with this on your Windows machine, you can use ActiveState Perl and install the Microsoft® Win32® version of SOAP::Lite. All of these examples were tested on the Win32 version of SOAP::Lite. There is detailed documentation available on the Web on how to install SOAP::Lite and all its prerequisites, so it is not covered here.

To begin, let's start with a simple C# WebMethod.

Connecting to the .NET-based Web Service

HelloWorld() – A Method That Takes Zero Parameters

Let's say you have a WebMethod that takes no arguments and returns a string:

[WebMethod]
public string HelloWorld()
{
   return "Hello World";
}

You can call this WebMethod in SOAP::Lite with this Perl code:

use SOAP::Lite;

my $soap = SOAP::Lite
    -> uri('http://www.alfredbr.com')
    -> on_action( sub { join '/', 'http://www.alfredbr.com', $_[1] } )
    -> proxy('https://localhost/Example1/Service1.asmx');

print $soap->HelloWorld()->result;

Rather than explain everything about SOAP::Lite (because the SOAP::Lite Web site does a better job of that than I can), let me explain what I had to do to get SOAP::Lite to work with .NET.

Here we are using three functions of the SOAP::Lite object. The first is uri(), which is the URI of your Web service. The argument to proxy() is the URL of your Web service. The on_action() function is explained in the section below.

SOAPAction

The code in the previous example creates an XML SOAP envelope and sends it to the URL you indicated in the proxy() function (via an HTTP POST). By default, SOAP::Lite creates a SOAPAction header that looks like [URI]#[method]. However, .NET requires the SOAPAction header to look like [URI]/[method].

You can see what the XML SOAP Request looks like by using a proxy such as tcpTrace.exe or proxyTrace.

The default header that SOAP::Lite sends looks like this:

POST /Example1/Service1.asmx HTTP/1.0
Accept: text/xml
Accept: multipart/*
Host: localhost
User-Agent: SOAP::Lite/Perl/0.55
Content-Length: 449
Content-Type: text/xml; charset=utf-8
SOAPAction: http://www.alfredbr.com/#HelloWorld

But .NET needs it to look like this:

POST /Example1/Service1.asmx HTTP/1.0
Accept: text/xml
Accept: multipart/*
Host: localhost
User-Agent: SOAP::Lite/Perl/0.55
Content-Length: 449
Content-Type: text/xml; charset=utf-8
SOAPAction: http://www.alfredbr.com/HelloWorld

A SOAPAction header entry is a hint to the Web service indicating the name of the method you wish to call. The SOAP specification does not specify the format of the SOAPAction HTTP header beyond indicating that it is a URI, so it is not surprising that SOAP::lite and .NET have different default formats. We will override the default SOAP::Lite implementation of on_action() with code that creates a SOAPAction URI that matches what the .NET-based Web service expects.

Our argument to on_action() is actually a subroutine. The name of the Web service method you are calling can be accessed by the Perl variable $_[1]. In our reimplementation of on_action() we have a simple subroutine that concatenates a slash and the name of the method to the URI value.

That is all you need to do for a method that takes no parameters.

Add() – A Method That Takes Two Parameters

Let's say you have a WebMethod that takes two integers and returns an integer:

[WebMethod]
public int Add(int x1, int x2)
{
   return x1 + x2;
}

You can call this WebMethod in SOAP::Lite with this Perl code:

use SOAP::Lite;

my $soap = SOAP::Lite
    -> uri('http://www.alfredbr.com/')
    -> on_action( sub { join '/', 'http://www.alfredbr.com', $_[1] } )
    -> proxy('https://localhost/Example1/Service1.asmx');

my $method = SOAP::Data->name('Add')
    ->attr({xmlns => 'http://www.alfredbr.com/'});

my @params = ( SOAP::Data->name(x1 => 3), 
               SOAP::Data->name(x2 => 4) );
 
print $soap->call($method => @params)->result;   

In this Perl code, we created a variable called $method that contains the name of the WebMethod we are calling. We also added an attribute to the method called xmlns.

Again, SOAP::Lite is creating an XML SOAP envelope and sending it (via a HTTP POST) to the server you designate in the proxy() function. This request must be in a style that the server is expecting. By default, a .NET-based Web service expects document/literal SOAP messages. In contrast, SOAP::Lite creates a SOAP request that uses RPC/encoded messages. You must make a choice of whether to change the .NET-based Web service to use RPC/encoded or ask SOAP::Lite to create a request that uses document/literal. I have chosen to use the .NET default document/literal messages.

Document/literal is the preferred way to encode your SOAP messages. RPC/encoded (also known as 'SOAP encoding') has been deprecated by the WS-I Basic Profile Working Group. For a thorough description of this, see Tim Ewald's article, The Argument Against SOAP Encoding.

Fortunately, we can force SOAP::Lite to use document/literal SOAP messages.

On the other hand, if you want to reconfigure your .NET-based Web service to use RPC/encoded SOAP messages, simply add the attribute [SoapRpcService] to the top of your .NET-based Web service class or add the [SoapRpcMethod] attribute before a particular method. For a thorough description of this, look at Customizing SOAP Messages in the .NET Framework Developer's Guide.

By default, SOAP::Lite will create an XML SOAP envelope body that looks like this:

<SOAP-ENV:Body>
   <Add>
      <x1 xsi:type="xsd:int">3</x1>
      <x2 xsi:type="xsd:int">4</x2>
   </Add>
</SOAP-ENV:Body>

If you take a look at the Web page that the Microsoft® Visual Studio® .NET debugger creates for you, it clearly shows that .NET expects a document/literal XML SOAP request to look like this:

<soap:Body>
   <Add xmlns="http://www.alfredbr.com/">
      <x1>3</x1>
      <x2>4</x2>
   </Add>
</soap:Body>

Don't worry about the type information that SOAP::Lite puts in the <x1> and <x2> tags. It will not cause any problems.

The following line in your code makes sure that when SOAP::Lite creates the <Add> element, it includes the attribute xmlns=http://www.alfredbr.com/.

my $method = SOAP::Data->name('Add')
    ->attr({xmlns => 'http://www.alfredbr.com/'});

The most interesting part comes next. Remember that what looks to you like a function call in Perl will actually be a XML document over the wire. The parameters that you pass in your function call must be included in the SOAP envelope. These parameters must be individually named. There is no concept of first parameter, second parameter, and so on. Each parameter value is enclosed in a XML element whose name exactly matches the name of the parameter in your WebMethod.

The parameters are held in a Perl associative array that we call @params. Take a look at this line of code:

my @params = ( SOAP::Data->name(x1 => 3), 
               SOAP::Data->name(x2 => 4));

Notice that the parameters are explicitly named. The first parameter in the Add() WebMethod is called x1, and x2 is the second parameter.

In the following line of code, the $method variable (which contains our method name and its attribute) and the @params array (which contains our named parameters and their values) are wrapped in a SOAP envelope and passed to the Web service via the call() function.

print $soap->call($method => @params)->result;

XML Soap Response

I'll cover this briefly since you can see this already in the test Web page that the Visual Studio .NET debugger creates for you. Here is the XML SOAP envelope from the response message:

<soap:Body>
   <AddResponse xmlns="http://www.alfredbr.com/">
      <AddResult>7</AddResult>
   </AddResponse>
</soap:Body>

Here we are returning a single value, so the response document looks pretty simple. Things can get a bit more complex when you return types such as .NET DataSets, which we examine later.

SOAP Headers

Just like HTML pages have a <head> and a <body>, SOAP envelopes have a header and a body. You can put any kind of data you want in your header. You may see authentication and auditing information stored in SOAP headers.

Writing a WebMethod that requires headers is very simple in .NET—you just add a class to your file and reference it with an attribute.

Let's say you have added a class to your Web service that looks like this:

public class MyHeader : SoapHeader 
{
   public string MyName;
}

And you make reference to this new class in your Web service class:

public MyHeader myHeader;

And you have a new WebMethod that looks like this:

[WebMethod, SoapHeader("myHeader")]
public string HelloWorld2()
{
   return "Hello World2";
}

After you recompile and attempt to run this method in the Visual Studio .NET debugger, you will notice something—you cannot call this method from within Microsoft— Internet Explorer anymore!

Internet Explorer is a great tool for debugging Web services because the .NET Framework creates a Web page that allows you to exercise your Web services. However, if your Web service uses SOAP-specific features (or does not support the HTTP GET or HTTP POST protocol), Internet Explorer cannot call your Web service.

The addition of SOAP headers has made this method a "SOAP-only" method. You will not be able to call this WebMethod from any client without providing the SOAP header. If you try to call this WebMethod without the SOAP header, you will not get your expected output. Instead you will get something called a "SOAP fault," which is described in the next section.

Turn on SOAP::Lite tracing by adding +trace => all to the first line of the file. This will enable us to see the error message in the trace output:

use SOAP::Lite +trace => all;

my $soap = SOAP::Lite
    -> uri('http://www.alfredbr.com/')
    -> on_action( sub { join '/', 'http://www.alfredbr.com', $_[1] } )
    -> proxy('https://localhost/Example1/Service1.asmx');

print $soap->HelloWorld2()->result;

When you run this Perl code with tracing turned on, you will get a lot of output. One of the lines will say:

   System.Web.Services.Protocols.SoapHeaderException: 
   Server did not find required MyHeader SOAP header in the message.

Here is the new Perl code that creates the SOAP header:

use SOAP::Lite maptype => {};

my $soap = SOAP::Lite
   -> uri('http://www.alfredbr.com/')
       -> on_action( sub{ join '/', 'http://www.alfredbr.com', $_[1] })
       -> proxy('https://localhost/Example1/Service1.asmx');

my $header = SOAP::Header->name(MyHeader => {
     MyName => "Alfred"
})->uri('http://www.alfredbr.com/')->prefix('');

print $soap->HelloWorld2($header)->result;

Note that I've introduced a new function called maptype on the first line of this program.

Maptype

By default SOAP::Lite serializes Perl hashes with a SOAPStruct type. This means the XML in your SOAP header will look like this:

<MyHeader xmlns="http://www.alfredbr.com/" xsi:type="namesp1:SOAPStruct">

But .NET expects the SOAP header to look like this:

<MyHeader xmlns="http://www.alfredbr.com/">

We do not want the default SOAPStruct type mapping so we set the maptype to the null set. To make this global, we combine it with 'use SOAP::Lite' at the top of the file:

use SOAP::Lite maptype => {}; 

We may chain multiple global settings on one line. For example, this line will turn on tracing and set the maptype to null.

   use SOAP::Lite ( +trace => all, maptype => {} );

In the Perl example above, the header is passed to the function in the same way that a parameter would have been passed, but remember that this WebMethod does not take any parameters! This syntax may look confusing, but the SOAP header is created properly.

Here is what the SOAP header and body look like:

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENC="https://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle=
"https://schemas.xmlsoap.org/soap/encoding/" 
xmlns:SOAP-ENV="https://schemas.xmlsoap.org/soap/envelope/" 
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
    <SOAP-ENV:Header>
        <MyHeader xmlns="http://www.alfredbr.com/">
            <MyName xsi:type="xsd:string">Alfred</MyName>
        </MyHeader>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
        <namesp1:HelloWorld2 xmlns:namesp1="http://www.alfredbr.com/"/>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Here is another example of a WebMethod that uses both SOAP headers and parameters.

[WebMethod, SoapHeader("myHeader")]
public int Add2(int x1, int x2)
{
   return x1 + x2;
}

And here is the Perl code that sends headers and parameters. This example is more complex, but easier to read:

use SOAP::Lite maptype => {};

my $soap = SOAP::Lite
       -> uri('http://www.alfredbr.com/')
       -> on_action( sub{ join '/', 'http://www.alfredbr.com', $_[1] })
       -> proxy('https://localhost/Example1/Service1.asmx');

my $header = SOAP::Header->name(MyHeader => {
     MyName => "Alfred"
})->uri('http://www.alfredbr.com/')->prefix('');

my @params = (
   $header,
   SOAP::Data->name(x1 => 3), 
   SOAP::Data->name(x2 => 4) 
);

my $method = SOAP::Data->name('Add2')
   ->attr({xmlns => 'http://www.alfredbr.com/'});

print $soap->call($method => @params)->result;

Here is what the entire HTTP request containing the SOAP envelope looks like:

POST /Example1/Service1.asmx HTTP/1.0
Accept: text/xml
Accept: multipart/*
Host: localhost
User-Agent: SOAP::Lite/Perl/0.55
Content-Length: 620
Content-Type: text/xml; charset=utf-8
SOAPAction: http://www.alfredbr.com/Add2

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENC="https://schemas.xmlsoap.org/soap/encoding/" 
SOAP-ENV:encodingStyle="https://schemas.xmlsoap.org/soap/encoding/" 
xmlns:SOAP-ENV="https://schemas.xmlsoap.org/soap/envelope/" 
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
   <SOAP-ENV:Header>
      <MyHeader xmlns="http://www.alfredbr.com/">
         <MyName xsi:type="xsd:string">Alfred</MyName>
      </MyHeader>
   </SOAP-ENV:Header>
   <SOAP-ENV:Body>
      <Add2 xmlns="http://www.alfredbr.com/">
         <x1 xsi:type="xsd:int">3</x1>
         <x2 xsi:type="xsd:int">4</x2>
      </Add2>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

Handling the Results

.NET WebService Studio

The use of SOAP headers in the example above make it impossible to test and debug the Web service using Internet Explorer. There is another utility called .NET WebService Studio, which allows us to test and debug all Web services, even if they use only the SOAP protocol. WebService Studio is used in a way similar to how Internet Explorer is used—just point it at the URL of your Web service and it will read the WSDL of the Web service and generate the proxy class for you. You are presented with an interface similar to the Web page that Visual Studio .NET generates for you, but it is much more flexible. You can create SOAP requests on the fly and browse the XML SOAP request and response in either raw or tree format. You can download .NET WebService Studio for free from GotDotNet.

BASIC Authentication

In the Introduction, I said that my users were using Perl on Solaris. The previous examples assume that the server has enabled Anonymous Access on the IIS Web server—no username or password is required. However, if you want to use basic authentication on your Web service, SOAP::Lite can send usernames and passwords upon request to your Web service.

To demonstrate SOAP::Lite support for basic authentication, first turn off Anonymous Access and turn on Basic Authentication for the virtual directory containing your Web service. If you are using a Windows machine as your client and not a UNIX machine, turn off Integrated Windows Authentication as well. (Be aware that when you turn off Integrated Windows Authentication for the virtual directory containing your Web service, you cannot debug the Web service in Visual Studio .NET.)

Once you turn on Basic Authentication, go back and run the last example. You should get an error message:

401 Access Denied at Example3.plx line 22

Now add these lines of code anywhere in your example (replacing the words username and password for a real Windows username and password on your Web server):

sub SOAP::Transport::HTTP::Client::get_basic_credentials { 
   return 'username' => 'password';
}

When your client application tries to access the Web service, the Web server issues a challenge for credentials. When SOAP::Lite receives this challenge, it calls the get_basic_credentials function. In this function, you arrange for the proper username and password pair to be returned to the Web server. You can watch this interaction with tcpTrace.exe.

Handling Errors

The "access denied" error message in the previous example was returned by Perl, not by our Web service. If you want to handle (and display) errors from the Web service, we have to do some extra work.

Any error messages that come from our Web service will be in the XML response document in the form of a SOAP fault. We can get access to the SOAP fault programmatically and print out the error message. (If we had turned on SOAP::Lite tracing, we could see the SOAP fault message in the trace output.)

Let's take a look at this block of code:

my $result = $soap->call($method => @params);

if ($result->fault)
{
   print $result->faultstring;
}
else
{
   print $result->result;
}

The first line stores the result of the WebMethod call in a variable called $result.

The if statement checks the value of the fault property of the $result object. If fault is non-zero, then an error must have occurred and we print the faultstring, otherwise there is no error and we print the result of our WebMethod.

This is an example of a SOAP body we receive if an error results from our call to our WebMethod:

<soap:Body>
   <soap:Fault>
   <faultcode>soap:Client</faultcode>
   <faultstring>
        System.Web.Services.Protocols.SoapException: Server did not    
        recognize the value of HTTP Header SOAPAction: 
        http://www.alfredbr.com/1.
      at System.Web.Services.Protocols.SoapServerProtocol.Initialize()
      at System.Web.Services.Protocols.ServerProtocolFactory.Create   
      (Type type, HttpContext context, HttpRequest request, 
      HttpResponse response, Boolean&amp; abortProcessing)
   </faultstring>
   <detail />
   </soap:Fault>
</soap:Body>

This error occurred because I did not code the on_action() handler correctly.

The Perl code above would have caught this error message and printed the error message text contained between the <faultstring> tags.

Accessing Complex Data with XPath

The examples shown so far have returned primitive data types such as strings or integers. It is possible to return complex data structures from a Web service and access individual elements of the data structure using XPath.

Let's say you have a C# structure that looks like this:

public class MailingAddress
{
   public string name;
   public string streetaddress;
   public string city;
   public string state;
   public string zipcode;
}

And you return this structure from a Web service that looks like this:

[WebMethod]
public MailingAddress GetAddress()
{
   MailingAddress myMailingAddress = new MailingAddress();

   myMailingAddress.name = "Able Baker";
   myMailingAddress.streetaddress = "123 Main Street";
   myMailingAddress.city = "Anytown";
   myMailingAddress.state = "VA";
   myMailingAddress.zipcode = "10203";

   return myMailingAddress;
}

The HTTP response from this Web service will look like this:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetAddressResponse xmlns="http://www.alfredbr.com/">
      <GetAddressResult>
        <name>string</name>
        <streetaddress>string</streetaddress>
        <city>string</city>
        <state>string</state>
        <zipcode>string</zipcode>
      </GetAddressResult>
    </GetAddressResponse>
  </soap:Body>
</soap:Envelope>

You can access the individual values returned by this Web service with this Perl code:

use SOAP::Lite;

my $soap = SOAP::Lite
    -> uri('http://www.alfredbr.com/')
    -> on_action( sub { join '/', 'http://www.alfredbr.com', $_[1] } )
    -> proxy('https://localhost/WebService1/Service1.asmx');

my $method = SOAP::Data->name('GetAddress')->attr({xmlns => 'http://www.alfredbr.com/'});

my $result = $soap->call($method);

print $result->valueof('//GetAddressResult/name'), "\n";
print $result->valueof('//GetAddressResult/streetaddress'), "\n";
print $result->valueof('//GetAddressResult/city'), "\n";
print $result->valueof('//GetAddressResult/state'), "\n";
print $result->valueof('//GetAddressResult/zipcode'), "\n";

The parameter that is passed to the valueof() function is a path to the values contained in the SOAP response envelope. SOAP::Lite implements only a subset of the XPath rules, so not all the XPath syntax is supported. Wildcards are supported, so these four lines all return the same value for this particular XML SOAP response.

   $result->valueof('//GetAddressResponse/GetAddressResult/name')
   $result->valueof('//GetAddressResult/name')
   $result->valueof('//*/*/name')
   $result->valueof('//*/name')
   $result->valueof('//name')

.NET DataSets

So far, we have covered how to call a simple WebMethod and how to pass header and parameter information. Next we cover returning .NET DataSets that can contain arbitrary row and column information from SQL Server queries.

Let's say you have a WebMethod that returns a .NET DataSet.

      [WebMethod]
      public DataSet simpleQuery()
      {
         string connectionString = 
            "Data Source=localhost;Integrated Security=SSPI;Initial Catalog=northwind";

         SqlConnection nwindConn = new SqlConnection( connectionString );

         SqlCommand selectCMD = new SqlCommand( 
               "SELECT CustomerID, CompanyName FROM Customers", 
               nwindConn);

         selectCMD.CommandTimeout = 30;

         SqlDataAdapter custDA = new SqlDataAdapter();
         custDA.SelectCommand = selectCMD;

         nwindConn.Open();

         DataSet custDS = new DataSet();
         custDA.Fill(custDS, "Customers");

         nwindConn.Close();

         return custDS;
      }

This WebMethod will connect to SQL Server using Windows Integrated Authentication, perform a query, and return the results to you as a DataSet.

You can call this WebMethod with the following Perl code:

use SOAP::Lite;

$SOAP::Constants::DO_NOT_USE_CHARSET = 1;

my $soap = SOAP::Lite
    -> uri('http://www.alfredbr.com')
    -> on_action( sub { join '/', 'http://www.alfredbr.com', $_[1] } )
    -> proxy('https://localhost/twoway/Service1.asmx');

my $method = SOAP::Data->name('simpleQuery')->attr({xmlns => 'http://www.alfredbr.com'});

my $result = $soap->call($method);

my @nodes = $result->valueof('//Customers');

foreach $node (@nodes)
{
   print $node->{'CustomerID'}, " - ", $node->{'CompanyName'}, "\n";
}

If you test the Web service with Internet Explorer, you can see the HTTP response containing the SOAP envelope:

HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <simpleQueryResponse xmlns="http://www.alfredbr.com">
      <simpleQueryResult>
        <xsd:schema>schema</xsd:schema>xml</simpleQueryResult>
    </simpleQueryResponse>
  </soap:Body>
</soap:Envelope>

Now take a look at the XML returned in the SOAP response envelope. The results of the SQL query are in the <NewDataSet> section in bold. (The rest of the XML in the SOAP response is the schema.)

<?xml version="1.0" encoding="utf-8" ?> 
<DataSet xmlns="http://www.alfredbr.com">
<xs:schema id="NewDataSet"  
xmlns:xs="http://www.w3.org/2001/XMLSchema" 
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
      <xs:element name="NewDataSet" msdata:IsDataSet="true">
         <xs:complexType>
            <xs:choice maxOccurs="unbounded">
               <xs:element name="Customers">
                  <xs:complexType>
                     <xs:sequence>
                        <xs:element name="CustomerID" type="xs:string" minOccurs="0" /> 
                        <xs:element name="CompanyName" type="xs:string" minOccurs="0" /> 
                  </xs:sequence>
                  </xs:complexType>
               </xs:element>
            </xs:choice>
         </xs:complexType>
      </xs:element>
   </xs:schema>
   <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" 
   xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
      <NewDataSet >
         <Customers diffgr:id="Customers1" msdata:rowOrder="0">
            <CustomerID>ALFKI</CustomerID> 
            <CompanyName>Alfreds Futterkiste</CompanyName> 
         </Customers>
         <Customers diffgr:id="Customers2" msdata:rowOrder="1">
            <CustomerID>ANATR</CustomerID> 
            <CompanyName>Ana Trujillo Emparedados y helados</CompanyName> 
         </Customers>
         <Customers diffgr:id="Customers3" msdata:rowOrder="2">
            <CustomerID>ANTON</CustomerID> 
            <CompanyName>Antonio Moreno Taquería</CompanyName> 
         </Customers>
      </NewDataSet>
   </diffgr:diffgram>
</DataSet>

The Web service returns a DataSet named "NewDataSet" and this new DataSet contains a table called "Customers". (The name "Customers" is specified in the Fill() function in the WebMethod.) A .NET DataSet can contain more than one table and each of the tables in a DataSet can be referenced by name.

In the SOAP response envelope, within the <NewDataSet> tags, a <Customers> tag for each row of data in your SQL query result set exists.

You can access the data in the Customers table in the form of a Perl array:

      my @nodes = $result->valueof('//Customers');

Once you have all the data from the Customers table in the form of a Perl array, you can access individual columns by name:

      foreach $node (@nodes)
      {
         print $node->{'CustomerID'}, " - ", $node->{'CompanyName'}, "\n";
      }

Custom Deserializer

This would not be Perl if there were not several alternate methods to access the data within a SOAP response. This next example utilizes a custom deserializer. Here the default deserializer is overridden with one from SOAP::Custom::XML::Deserializer.

use SOAP::Lite;

my $soap = SOAP::Lite
    -> uri('http://www.alfredbr.com')
    -> on_action( sub { join '/', 'http://www.alfredbr.com', $_[1] } )
    -> proxy('https://localhost/twoway/Service1.asmx');

$soap->deserializer(SOAP::Custom::XML::Deserializer->new);

my $dataset = $soap->simpleQuery()->valueof('//NewDataSet');

for my $customer ($dataset->Customers)
{
        print $customer->CustomerID, " - ", $customer->CompanyName, "\n";
}

Conclusion

There are a few techniques that you need to know to get SOAP::Lite clients to call .NET-based Web services, and there are some assumptions that SOAP::Lite makes about how the data will be represented. The examples shown in this article demonstrate how applications written using Perl with the SOAP::Lite library can use Microsoft .NET-based Web services as an effective technique for exposing server-side data to a variety of client programs.

For More Information

The Argument Against SOAP Encoding

An Introduction to SOAP

PocketSOAP home page

Quick Start Guide with SOAP and SOAP::Lite

Simple Object Access Protocol (SOAP) 1.1

Simplified SOAP Development with SOAP::Lite - Part I

SOAP Cookbook

SOAP::Lite Client HOWTO

SOAP::Lite Contents

SOAP::Lite for Perl home page

SOAP::Lite Interoperability

Using XML-RPC for Web services : Using SOAP::Lite with Perl

XML Path Language (XPath)
Version 1.0