Topics in This Chapter
Web Service Overview: The popularity of Web Services stems from the fact that they are implemented using the non-proprietary technologies of HTTP, XML, and SOAP. For them to become truly useful, users must have a way to locate them on the Internet. UDDI offers one such discovery service.
Creating a Web Service: In C#, a Web Service is created by implementing a method that is exposed to clients across the Internet. To indicate a method is available, a WebService
directive is required on the Web page, and a WebMethod
attribute is attached to the Web Service method.
Building an XML Web Service Client: The easiest way to create a Web Service client is to build a proxy using the Web Services Description Language (WSDL) provided by the Web Service. The proxy handles the physical connection to the Web Service and provides asynchronous and synchronous access.
WSDL and SOAP: WSDL provides the information that describes a Web Service; Simple Object Access Protocol (SOAP) is the format in which the requests and responses are delivered. An understanding of both offers added insight into how Web Services operate.
Web Service Examples: To satisfy real-world business needs, Web Services must deliver more than simple data types. Examples in this section show how to build a service that delivers images and how to build a client to retrieve datasets from the publicly available Amazon Web Services.
XML Web Services provide a relatively simple technique for accessing a method on an object that is running on a local or remote computer. Many embrace this lightweight approach to making Remote Procedure Calls (RPC) as technology that will spell the end to much heavier and complex solutions for distributed communications such as DCOM and CORBA. At the root of its appeal is the fact that Web Services are based on standardized technologies such as HTTP and XML that are designed to promote seamless interoperability among different operating systems. Not everyone is sold on them. Critics point out the lack of security standards, the heavy bandwidth requirements of XML, and the lack of notification and transaction services.
The chapter takes a practical look at the pluses and minuses of implementing and consuming Web Services in a .NET environment. It presents Web Services from the perspective of both the server and client. On the server side, the chapter explores how to define a Web Services class, make its methods available to HTTP clients, and access the ASP.NET Application and Session objects. The emphasis on the client side is how to use the Web Services Description Language (WSDL) contract to create a proxy class that implement calls to the Web Service.
HTTP and SOAP—a protocol that codifies how XML is used to package the request and response data that comprise a Web Service operation—are the two cornerstones of Web Services. Other protocols such as GET and POST are available, but the Simple Object Access Protocol (SOAP) has fewer limitations and is used in the majority of real-world applications. This chapter takes a look at the SOAP format as well as several issues related to SOAP, including the handling of complex data types, exception handling, and security.
We begin the exploration of the .NET Web Services architecture by creating a simple Web Service and Web Service consumer. Our first examples are created “by hand” to emphasize the underlying principles. After these are understood, we see how Visual Studio.NET simplifies the development. The chapter also demonstrates how to use the wsdl.exe
tool and .NET classes that are integral to the development process. Closing out the chapter is a sample application to access the Amazon Web Services.
The Web Service architecture is a service-oriented architecture that enables applications to be distributed across a network or the Internet to clients using any language or operating system. As shown in Figure 18-1, Web Service communications are implemented using existing technology and standards that require no proprietary vendor support. This technology either formed the basis of the Internet or evolved from it. HTTP and XML have been discussed in earlier chapters, but let's take a brief look at them—from a Web Services perspective—along with TCP/IP and SOAP:
TCP/IP (Transmission Control Protocol/Internet Protocol). A communications protocol suite that forms the basis of the Internet. It's an open system that governs the flow of data between computers by breaking data into chunks that are easily routed over a network. A Web Service user or developer rarely has direct contact with this layer.
HTTP (Hypertext Transfer Protocol). Technically, this text-based protocol is a Remote Procedure Call (RPC) protocol that supports request/response communications. The .NET Framework as well as most production Web Services use it because it has wide support and generally allows information to sail unimpeded through firewalls. Both the HTTP POST
and HTTP GET
methods are supported in .NET as a way to call a Web Service. Most applications use POST
, because it packages the request inside the request body (in a SOAP envelope), rather than as part of a less secure query string.
The rules that describe how a message is carried within or on top of a protocol is referred to as a binding. The default protocol binding used by .NET for Web Services is HttpSoap
, which should be used to access Web Services that understand SOAP. It is specified inside the System.Web
section of the machine.config
file.
XML (Extended Markup Language). We have seen in earlier chapters how data can be serialized into XML format for storage or transmission. The fact that it is text based makes it easy to work with. Just about every programming environment supports tools for encoding and decoding XML formatted data. Its inherent flexibility, extensibility, and validity checking make it attractive to Web Services that must deal with simple data types such as strings, as well as more complex data structures. There are currently two XML-based protocols used for delivering Web Services: XML-RPC and SOAP. Because .NET supports SOAP, this chapter focuses on it. After you understand SOAP, you should have no difficulty with XML-RPC if you encounter it.
SOAP (Simple Object Access Protocol). SOAP is defined as “a lightweight protocol for exchange of information in a decentralized, distributed environment.”[1] It is not designed specifically for Web Services, nor restricted to HTTP; but its RPC specifications define a model for invoking methods on a specified target machine, passing parameters, handling faults, and receiving a method response. We will look at SOAP in detail later in the chapter. For now, keep in mind that the details of XML and SOAP are typically handled transparently by .NET. The provider code only needs to focus on implementing the method that returns the desired data; the requestor code simply places a method call and processes the returned data.
To use a Web Service, a client must have a description of how to access the service. This information is provided by a Web Services Description Language (WSDL) document that provides the name of the service, the signature of the method(s) that can be called, the address of the service (usually a URL), and binding information that describes how the transport operation will occur. In practical terms, the WSDL information contains the methods that actually call a Web Service. We implement these methods in our client code (as source or a DLL reference) and use them as a proxy to access the service. Section 18.2, “Building an XML Web Service,” provides a concrete example of using .NET to retrieve WSDL information and incorporate it into a client application.
Web sites are identified by a domain name and IP address that are maintained by a distributed network service known as the Domain Name System (DNS). Servers in this network are responsible for controlling e-mail delivery and translating domain names into IP addresses. The combination of DNS and Web search engines enables users to quickly locate Web content. However, neither DNS servers nor search engines provide a formal way of identifying Web Services.
To organize Web Services into a publicly searchable directory, a public consortium (www.uddi.com) comprising hundreds of companies has defined a standard known as Universal Description Discovery and Integration (UDDI). This standard defines a SOAP-based interface that can be used to publish a service or inquire about services in a UDDI-compliant registry. The registry has a business-to-business flavor about it—containing information about a company, its services, and interface specifications for any Web Services it offers. Importantly, there is no single UDDI registry. IBM, SAP, and Microsoft maintain the most prominent registries. Users may query each separately by entering a business name or service as a search term. Figure 18-2 provides an overview of the inquiry process.
The dialog between the client and UDDI registry server is conducted using SOAP messages. Overall, UDDI defines approximately 40 SOAP messages for inquiry and publishing.
To demonstrate how to use UDDI, w'e'll look at the SOAP messages sent between client and server as we seek to discover a Web Service that can provide a stock quote. A Web-based UDDI browser (described shortly) sends and receives the messages.
For our example, we'll search the UDDI registry provided by Microsoft:
http://uddi.microsoft.com/inquire
An inquiry may request business information or tModel
information. The former contains information about a business, including contact information and a description of services. The tModel
structure is much simpler: It consists of a unique key, optional description, and a URL or pointer to a Web page where information for using the service is described. The following message requests a list of tModel
keys for companies whose service includes providing a stock quote.
<Envelope> <Body> <find_tModel generic="1.0" maxRows="100"> <findQualifiers/> <name>stock quote</name> </find_tModel> </Body> </Envelope>
A list of tModel
keys is returned. These keys are used for the subsequent query:
<soap:Envelope> <soap:Body> <tModelList generic="1.0" operator="Microsoft Corporation" > <tModelInfos> <tModelInfo tModelKey="uuid:7aa6f610-5e3c-11d7-bece-000629dc0a53"> <name>Stock Quote</name> </tModelInfo> <tModelInfo tModelKey="uuid:265973ab-31cb-4890-83e0-34d9c1b385e5"> <name>Stock Quotes and Information</name> </tModelInfo> </tModelInfos> </tModelList> </soap:Body> </soap:Envelope>
Send a request for tModel
details for the service with the specified tModelKey
:
<Envelope>
<Body>
<get_tModelDetail generic="1.0">
<tModelKey>uuid:7aa6f610-5e3c-11d7-bece-000629dc0a53
</tModelKey>
</get_tModelDetail>
</Body>
</Envelope>
The response message includes the OverviewURL
element that points to a WSDL document. This document contains the information needed to create an application to access the service.
<overviewDoc>
<description xml:lang="en">
Get Stock quote for a company symbol
</description>
<overviewURL>
http://www.webservicex.net/stockquote.asmx?WSDL
</overviewURL>
</overviewDoc>
You can display the WSDL by pointing your browser to this URL. To invoke the Web Service, remove the query string (?WSDL
) from the URL, and navigate to it with your browser.
To interact with a UDDI registry—whether to publish or inquire—you need a way to generate SOAP requests and interpret responses. One option is to write your own application using the Microsoft UDDI 2.0 SDK. It contains excellent C# examples that demonstrate the API and can be run against a test UDDI registry hosted by Microsoft.
For making registry inquires, Web-based UDDI browsers are available. A publicly available one—as well as a wealth of accompanying UDDI information—can be found at
http://www.soapclient.com/UDDISearch.html
In this section, we demonstrate how to build a Web Service by hand and then access it using a browser. We also show how to create the same Web Service using Visual Studio.NET. Although IIS is used, the examples run on any Web server.
The first step is to select or create a virtual directory under IIS that will hold the Web Service source code file(s). Any physical directory can be mapped to an IIS virtual directory. You can use the Internet Service Manager or simply right-click the directory and select Sharing-Web Sharing. Then, assign it an alias that will be used in its URL. In our example, we will place the service in the ws
subdirectory.
After the directory has been set up, the next step is to use a text editor to create a file in this directory with the .asmx
extension to contain the Web Service code. Listing 18-1 contains the code for our simple Web Service. This service exposes a method GetDayBorn
that accepts three integer parameters that represent the month, day, and year for birth date. A string value containing the day of the week (Monday, Tuesday, and so on) for this date is returned. The service performs rudimentary error checking and returns an error message if the date is invalid.
Example 18-1. Web Service to Return a Date's Day of Week—BirthDayWS.asmx
<%@ WebService Language="C#" Class="BirthDayWS.BirthDay" %> using System; namespace BirthDayWS { public class BirthDay { [System.Web.Services.WebMethod (Description="Return day of week for a date")] public string GetDayBorn(int mo, int day, int yr) { bool err = false; string dob; if (mo <1 || mo >12) err=true; if (day < 1 || day > 31) err=true; if (err) dob = "Invalid Date"; } else { DateTime dt = new DateTime(yr,mo,day); dob = dt.ToString("dddd"); // Get day } return(dob); } } }
The code consists of a single class and a method that is invoked by a client to return the day-of-week value. In addition to the C# code that implements the logic of the service, two other elements are required: a WebService
directive and a WebMethod
attribute.
The WebService
directive identifies the file as defining a Web Service:
<%@ WebService Language="C#" Class="BirthDayWS.BirthDay" %>
The directive specifies the class implementing the XML Web Service and the programming language used in the implementation. In this example, the directive and the code for the class are present in the BirthDayWS.asmx
file. Note, however, that the class can be in a separate assembly. In that case, the separate assembly is placed in a in
directory below the Web application where the Web Service resides. If the class were in bdAssembly.dll
, the WebService
directive would look like this:
<%@ WebService Language="C#" Class="BirthDayWS.BirthDay, bdAssembly" %>
This statement would be the only line needed in the .asmx
file.
The WebMethod
attribute identifies a method as being accessible to clients making HTTP requests—that is, as an XML Web Service. Although not required, it is a good practice to include the Description
property to describe the purpose of the method:
[System.Web.Services.WebMethod (Description="Return day of week for a date")]
The description is added to the WSDL for the service and—as we will see next—is displayed when a Web Service is accessed via a browser. The WebMethod
attribute has other optional parameters, which are described later in this chapter.
A quick way to test the newly developed Web Service is to point a browser to its location. For this example, we enter the address:
http://localhost/ws/BirthDayWS.asmx
This brings up a Web page that lists all the services (methods) available through this .asmx
file as well as a Service Description link that displays WSDL information. For our example, there is only one method, GetDayBorn
. Clicking it yields the page shown in Figure 18-3. This page contains the name of the class implementing the Web Service, the name of the method to be invoked, a description of the method, and text boxes for entering values to be passed to the Web Service method.
To use the Web Service, fill in the parameter values and select the Invoke button. This causes the parameters to be sent to the Web Service using the HTTP POST
protocol. The output received from the service is an XML document shown in Figure 18-4.
The output from the method is included in the string
element of the XML wrapper. Fortunately, we do not have to parse the XML to retrieve this value when writing our own SOAP client. The WSDL contract provides information that allows our client to treat the remote Web Service as a method that returns data conforming to the method's type—not as XML.
It is unnecessary to compile the .asmx
file containing the Web Service in order to deploy it. On a Microsoft Windows server, the ASP.NET runtime automatically compiles the file the first time it is requested and places it in a subdirectory—named after the virtual directory—on the following path:
<%windir%>Microsoft.NETFramework<version> Temporary ASP.NET Files
To view the WSDL associated with this Web Service, open your browser and append ?WSDL
to the URL of the .asmx
file:
http://localhost/ws/BirthDayWS.asmx?WSDL
Aside from the usual advantages of IntelliSense and single key compilation (F5), the major advantage of VS.NET over a manual approach is that it automatically creates a new virtual directory under IIS to host the Web Service. This directory takes the same name as the project.
To create a Web Service with VS.NET, open it up and select ASP.NET Web Service as the project type. Assign it a project name (BirthDayWS
, in this case) that reflects the purpose of the Web Service. When the project opens, select View Code and you will find that the following template class is predefined:
namespace BirthDayWS
{
public class Service1 : System.Web.Services.WebService
{
public Service1()
{ InitializeComponent(); }
private IContainer components = null;
private void InitializeComponent() { }
protected override void Dispose( bool disposing )
{
if(disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
// --> Place BirthDayWS code here
}
}
To implement the service, rename the class to Birthday
and add the code for the GetDayBorn
method. Use the browser to test the service by either compiling the code, which automatically invokes the browser, or directly pointing the browser to
http://localhost/BirthDayWS/BirthDayWS.asmx
The only significant difference between this code and our handcrafted version is that the Web class now inherits from WebService
rather than the default System.Object
. The service works either way, but by inheriting from WebService
it gains the functionality of an ASP.NET application.
The WebService
base class exposes properties to a Web Service that enable it to access the intrinsic ASP.NET objects introduced in Chapter 17, “The ASP.NET Application Environment.”
Application
. Provides access to an Application
object that can be used to maintain state information for all sessions accessing the Web Service. For example, it can be used to keep track of how many times the service is called.
Session
. Provides access to the HttpSessionState
object that maintains session state information. It can be used, for example, to track how many times a service is called while the current session is alive. This property is available only if the EnableSession
property of the WebMethodAttribute
is set to true
.
Context
. Exposes the HttpContext
class. Recall that this class provides a wealth of information about the current request.
User
. Returns the IPrincipal
object that provides user authentication information and can be used to determine whether the request is authorized (see Chapter 17).
To demonstrate the use of state information, let's extend our example to use the Application
object to keep track of the number of times the Web Service is called.
Our first step is to add the statement in bold to the GetDayBorn
method:
dob = dt.ToString("dddd"); // extracts day
this.Application["count"] = GetCount()+1;
Next, add a method to the class that internally returns the “count” value of the Application
object. Finally, add a method, GetVisitors
, which allows clients to view the number of calls to the Web Service as a Web Service call:
private int GetCount() { object ob = this.Application["count"]; if (ob == null) { return(0); } else { return((int)ob); } } [WebMethod(Description="Number of times Web Service called.")] public int GetVisitors() { return (GetCount()); }
The BirthDayWS
example demonstrates that a functioning Web Service can be created with a minimal use of .NET classes. However, before releasing a Web Service to the Internet, there are additional features that should be considered. For instance, each XML Web Service should be given a unique namespace rather than relying on the default <http://tempuria.org>, caching can be implemented to improve performance, and overloaded methods can be implemented using the WebMethod
attribute.
The optional WebService
attribute (not to be confused with the WebService
directive) is applied to the class implementing the XML Web Service. It has two useful properties: Description
, which describes the overall Web Service, and Namespace
, which sets the default XML namespace for the service. In general terms, namespaces are used to avoid naming conflicts among XML elements and attributes. In this case, it is used to make the Web Service name unique.
Let's add a WebService
attribute containing a namespace and description to the class in our BirthDayWS
example:
[WebService(Namespace="http://www.dateservices.com/ws/", Description="<b>Web Service that Provides Date Functions.</b>")] public class BirthDay : System.Web.Services.WebService
It is important to note that the namespace is intended to be a unique identifier, and does not need to actually point to anything. Domains are typically used to take advantage of their uniqueness.
Figure 18-5 shows the page that is returned when we call the Web Service that is now updated with the WebService
attribute and the GetVisitors
method.
As mentioned previously, the WebMethod
attribute is required to expose a method as part of a Web Service. Its properties include Description
, which we have already used, EnableSession
, MessageName
, CacheDuration
, and TransactionOption
. The latter property applies to applications that call transactional COM+ components—a topic not discussed in this book. The other three properties are useful for developing general Web Services. Let's look at the role of each.
This property is used with Web Services that inherit from the WebService
class. Turning it on allows a Web Service method to access session information through WebService.Session
or HttpContext.Current.Session
. By default, this is set to false
, because maintaining session data increases memory consumption and reduces performance.
Example: [WebMethod(EnableSession=true)] // Default is false
Be aware that Windows Forms clients do not provide the same support for session state variables that a browser does. Sessions rely on cookies to preserve state data. Unlike browsers, Windows Forms applications do not store cookies. Thus, the service thinks that each request is the first request. There is a work-around, however, which is described in the following section on building a Web Services client.
Suppose we want to add a method to our Web Service that accepts the month as a string rather than an integer. Because the signature is different than the original GetDayBorn
method, we can give it the same method name and C# will compile it as an overloaded method. However, when we try to access it from a browser, we receive a system exception indicating that the two methods have the same name.
The problem lies in the WSDL, which requires that each Web method in its XML elements be uniquely named, irrespective of its signature. By default, the routine in ASP.NET that generates WSDL code uses the name for both. The solution—aside from renaming the method—is to use the MessageName
property to indicate a surrogate name.
[WebMethod(Description="Return day of week for any date",
MessageName="GetDayBorn2")]
public string GetDayBorn(string month, int day, int yr){
// Code to convert string month to int value
string allMos= "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC";
month = month.Substring(0,3).ToUpper();
int ndx = allMos.IndexOf(month);
if(ndx <0) err=true; else mo = ndx/3 +1;
// Remainder of code goes here...
Just like ASP.NET Web pages, output from a Web method can be cached to obviate executing it each time the method is called. The CacheDuration
property specifies how long (in seconds) the output is to be cached. For methods that accept arguments, cached output is saved for each unique set of arguments.
Example: [WebMethod(CacheDuration=1800)] // Value is in seconds
The only rule to follow when (if) setting up caching is to do it judiciously. Caching greatly improves performance for methods that are requested frequently and implement logic that requires lengthy processing. On the other hand, methods such as GetDayBorn
in our example can actually hurt performance because most requests to it will yield unique results.
.NET Web Services support four protocols: HttpGet
, HttpPost
, HttpSoap
, and HttpLocalHost
. Of these, HttpPost
and HttpGet
are disabled for security reasons. The effect of this is to limit Web Service access to URLs located on http://localhost
; an attempt by a remote machine to access a local Web Service results in this message being displayed:
The test form is only available for requests from the local machine
To make a Web Service available to remote users, it is necessary to enable the HttpPost
and HttpGet
protocols. This is easily done by configuring the <protocol>
section of the web.config file to “add” the two protocols.
<system.web> <webServices> <protocols> <add name="HttpGet"/> <add name="HttpPost"/> </protocols> </webServices> </system.web>
Recall that navigating to a Web Services page with no parameters brings up a help page that describes how to use the service, and provides links to invoke the Web Service methods or display the WSDL. If you do not want to expose this information, you can disable the display of help pages by removing the Documentation
protocol. To do so, place the following statement in the <protocols>
element of the web.config
file:
<remove name="Documentation" />
This section describes how to create a client application that consumes a Web Service. The sobjective is to make the client's call to the remote Web method as simple as calling a method in its own code. Although the actual implementation does not reach quite that level of simplicity, the code is straightforward, and much of it can be generated automatically using .NET tools.
Before delving into details, let's first take a high-level view of how a .NET Web Services client interacts with a Web Service.
The most important thing to observe in Figure 18-6 is that the client does not directly invoke the Web Service method. Instead, it calls a proxy object that performs this task. The proxy class is created from the WSDL information provided by the Web Service. We'll see how to do this shortly. The proxy code is a class that has the same class name and method names as the Web Service class does. It contains the transport logic that allows it to make the actual connection to the Web Service. It may do this either synchronously (without receiving confirmation) or asynchronously. The messages exchanged between the proxy and server are bundled within an HTTP request and transmitted using either the HTTP or the SOAP wire protocol.
To demonstrate the basic principles involved in creating a Web Service client, let's develop a console client application to access the BirthDayWS
Web Service (refer to Figure 18-1). The client passes three arguments to the service and prints the string it returns. Recall that the service consists of the Web class Birthday
and a single method GetDayBorn
:
public class BirthDay { [System.Web.Services.WebMethod (Description="Return day of week for a date")] public string GetDayBorn(int mo, int day, int yr)
Although the Web Service method can reside on a remote machine anywhere on the Internet, we can approach the client design as if we were within the same assembly. Here is the client code contained in the file bdClient.cs
:
using System; using System.Web.Services; public class BirthDayClient { static void Main(string[] args) { BirthDay bd = new BirthDay(); string dayOfWeek = bd.GetDayBorn(12,20,1963); Console.WriteLine(dayOfWeek); } }
Compiling this, of course, results in an error stating that BirthDay
and bd
cannot be found. We resolve this by creating a proxy class that performs the remote call, yet can be accessed locally by the client. The code for the proxy class is obtained by feeding the WSDL information that defines the Web Service into the .NET wsdl.exe
utility.
The wsdl.exe
utility reads the WSDL describing a Web Service and generates the source code for a proxy class to access the service; it can also use the information to create the skeleton code for a Web Service. This latter feature is designed for developers who prefer to design the WSDL as the first step in creating a Web Service. We do not take that approach in this chapter, but you should be aware that there are WSDL editors available for that task.
The wsdl.exe
utility is run from the command line and has numerous flags or options to govern its execution. Table 18-1 lists those most commonly used.
Table 18-1. wsdl.exe
Command-Line Options
Option | Description |
---|---|
/appsettingurlkey /urlkey: | Specifies a key within the client's |
/language /l: | Specifies the language to use for generating the proxy class. Choices are:
C# is the default. |
/namespace /n: | Specifies the namespace for the proxy. |
| Specifies file in which the generated proxy code is placed. The default is to use the XML Web Service name with an extension reflecting the language used for the code. |
| The wire protocol to use within the proxy code:
|
| Generates an abstract class for the Web Service. This is used when the WSDL document is used to create a Web Service. |
| Generates interfaces—rather than abstract classes—for the Web Service. Available only in 2.0 and later. |
The following statement creates a proxy class from the BirthDayWS.asmx
Web Service and places it in c:BDProxy.cs
:
wsdl.exe /out:c:BDProxy.cs http://localhost/ws/ BirthDayWS.asmx?WSDL
This proxy source code can be used in two ways: include it in the client's source code, or compile it into a DLL and add a reference to this DLL when compiling the client code. Let's look first at the DLL approach. This command line compiles the source code and links two DLL files containing classes required by the proxy:
csc /t:library /r:system.web.services.dll /r:system.xml.dll BDProxy.cs
We are now ready to compile the client source code into an executable file, bdClient.exe
:
csc /r:BDProxy.dll bdClient.cs
If we add the proxy code directly to the bdClient.cs
file and compile it, the result is a module that produces the same output as a version that links the proxy as a separate DLL file.
When wsdl.exe
creates proxy code, it checks the associated Web Service for compliance with the WS-I Basic Profile 1.0 standards and emits a warning if the service is noncompliant. For information on this standard, refer to the Web Services Interoperability Organization's Web site at http://www.ws-i.org.
Listing 18-2 examines the proxy source code to understand how it converts a client's call into a remote Web Service invocation.
Example 18-2. Proxy Source Code to Access BirthDayWS.asmx
Web Service
using System; using System.ComponentModel; using System.Diagnostics; using System.Web.Services; using System.Web.Services.Protocols; using System.Xml.Serialization; // // Auto-generated by wsdl, Version=2.0.40607.16. [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute( Name="BirthDaySoap", Namespace="http://tempuri.org/")] public class BirthDay : SoapHttpClientProtocol { private System.Threading.SendOrPostCallback GetDayBornOperationCompleted; public BirthDay() { this.Url = "http://localhost/ws/BirthDayWS.asmx"; } public event GetDayBornCompletedEventHandler GetDayBornCompleted; // (1) Synchronous Call to Web Service [SoapDocumentMethodAttribute( "http://tempuri.org/GetDayBorn", RequestNamespace="http://tempuri.org/", ResponseNamespace="http://tempuri.org/", Use= System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle= System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] public string GetDayBorn(int mo, int day, int yr) { object[] results = this.Invoke("GetDayBorn", new object[] { mo, day, yr}); return ((string)(results[0])); } // (2) Asynchronous call to Web Service public System.IAsyncResult BeginGetDayBorn(int mo, int day, int yr, System.AsyncCallback callback, object asyncState) { return this.BeginInvoke("GetDayBorn", new object[] { mo, day, yr}, callback, asyncState); } public string EndGetDayBorn(System.IAsyncResult asyncResult) { object[] results = this.EndInvoke(asyncResult); return ((string)(results[0])); } // (3) Call this for event-based asynchronous handling public void GetDayBornAsync(int mo, int day, int yr) { this.GetDayBornAsync(mo, day, yr, null); } public void GetDayBornAsync(int mo, int day, int yr, object userState) { if ((this.GetDayBornOperationCompleted == null)) { this.GetDayBornOperationCompleted = new System.Threading.SendOrPostCallback( this.OnGetDayBornOperationCompleted); } this.InvokeAsync("GetDayBorn", new object[] { mo, day, yr}, this.GetDayBornOperationCompleted, userState); } private void OnGetDayBornOperationCompleted(object arg) { if ((this.GetDayBornCompleted != null)) { InvokeCompletedEventArgs invokeArgs = ((InvokeCompletedEventArgs)(arg)); this.GetDayBornCompleted(this, new GetDayBornCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); } } public new void CancelAsync(object userState) { base.CancelAsync(userState); } } public delegate void GetDayBornCompletedEventHandler( object sender, GetDayBornCompletedEventArgs args); public class GetDayBornCompletedEventArgs :AsyncCompletedEventArgs { private object[] results; internal GetDayBornCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : base(exception, cancelled, userState) { this.results = results; } // Results are available as a property public string Result { get { this.RaiseExceptionIfNecessary(); return ((string)(this.results[0])); } } }
Observe that the proxy class has the same name as the Web Service class (Birthday
) it represents, and implements a method (GetDayBorn
) having the same name as the Web Service method. The supporting code is quite different, however. It contains transport logic rather than application logic. This code uses methods provided by the System.Web.Services.Protocols.SoapHttpClientProtocol
class from which the proxy class derives. Table 18-2 summarizes of the more useful members of the class.
Table 18-2. Selected Members of SoapHttpClientProtocol
Member | Description |
---|---|
| Begins an asynchronous invocation of the Web method. |
| Ends an asynchronous invocation of the Web method. |
| Invokes the Web method synchronously. |
| Gets or sets the collection of cookies. This permits a proxy-based client to accept cookies and allow a server to maintain state information. |
| Gets or sets proxy information needed to make a Web Service call through a firewall. |
| Gets or sets the timeout (in milliseconds) a client waits for a synchronous call to a Web Service to be completed. |
| Gets or sets the URL used to access the Web server. |
Let's look at how these members are used in a proxy class.
A proxy provides the capability to invoke a Web Service method synchronously or asynchronously. Recall from Chapter 13, “Asynchronous Programming and Multithreading,” that a synchronous operation uses only one thread, which is blocked until control returns from the called method. An asynchronous call, on the other hand, creates a worker thread that handles the operations of the called method. Control returns immediately to the main thread while a separate thread handles the auxiliary task. The calling routine is notified when the task is completed.
Synchronous communication is implemented with a proxy method that has the same name as the Web method and uses the Invoke
method to request the service.
public string GetDayBorn(int mo, int day, int yr) {
object[] results = this.Invoke("GetDayBorn", new object[] {
mo, day, yr});
return ((string)(results[0]));
}
This is the most intuitive way to access a service because a client can be totally unaware of the proxy and execute a call to the Web Service method, using its actual name. However, because a synchronous call by definition does not require a response, the client code should include a timeout interval and handle any exception that occurs if the interval is exceeded. The following code sets a timeout of four seconds and handles any System.Net.WebException
that is thrown.
// Must add using System.Net; BirthDay bd = new BirthDay(); bd.Timeout= 4000; // Set timeout to 4 seconds try { string dayOfWeek = bd.GetDayBorn(12,20,1963); Console.WriteLine(dayOfWeek); } catch (WebException ex) { Console.WriteLine(ex.Message); // Will report timeout } catch (Exception ex) { Console.WriteLine("Unknown error occurred."); }
Asynchronous communication is performed using the Begin
methodname
and End
methodname
methods supplied by the proxy. Internally, these call the BeginInvoke
and EndInvoke
methods, respectively. BeginInvoke
queues the method to be run on a separate (worker) thread. It returns an IAsyncResult
type that is subsequently passed to the EndInvoke
method to retrieve the results. As shown in the following code, the IAsyncResult
type is also used to determine if the call has been completed.
BirthDay bd = new BirthDay();
// Pass null callback argument since we will poll status
IAsyncResult dayOfWeek =bd.BeginGetDayBorn(12,20,1963,null,null);
// Perform other operations here while waiting for response
If (dayOfWeek.IsCompleted)
Console.Write(bd.EndGetDayBorn(dayOfWeek));
This “polling” approach can be used to periodically check the status of the request. Another approach is to have the Web Service call a method in the client code when the response is ready. The AsyncCallback
delegate is used to specify the method that receives the callback.
// Create delegate that specifies method to be called back AsyncCallback wscb = new AsyncCallback(DayReturned); BirthDay bd = new BirthDay(); IAsyncResult dayOfWeek = bd.BeginGetDayBorn(12,20,1963,wscb,null); // This method is called back when the web method completes public static void DayReturned(IAsyncResult result) { BirthDay bd = new BirthDay(); Console.WriteLine(bd.EndGetDayBorn(result)); }
As with polling, the method implementing EndInvoke
(EndGetDayBorn
, in this example) is called to return the result.
If you take a close look at the proxy code in Listing 18-2, you'll notice that it includes a delegate and corresponding event declaration:
public event GetDayBornCompletedEventHandler GetDayBornCompleted; public delegate void GetDayBornCompletedEventHandler( object sender, GetDayBornCompletedEventArgs args);
These enable client code to treat the Web Service invocation as an event that can be handled by a local event handler. Only a few lines of code are required to hook up an event handler to the event defined in the proxy:
Birthday service = new Birthday(); // (1) Associate event handler with event service.GetDayBornCompleted += new GetDayBornCompletedEventHandler(this.ShowOutput); // (2) Invoke service asynchronously service.GetDayBornAsync(); // (3) Event handler called when service returns value private void ShowOutput(object sender, GetDayBornCompletedEventArgs args) { Console.WriteLine(args.Result); }
The Web Service is invoked asynchronously by calling the proxy provided method GetDayBornAsync
(<web service name>Async)
. When the Web Service finishes execution, .NET returns the results to the event handler through the args
parameter.
Setting a Web Service method's EnableSession
attribute to true
enables the method to maintain state information. However, this is only effective if the client can accept cookies. This is not usually a problem with browsers, but Windows Forms (WinForms) applications do not store cookies by default. The CookieContainer
property is a way to permit WinForms clients to accept cookies.
To permit the client to accept cookies, set the CookieContainer
property of the proxy class instance to a CookieContainer
object. The code shown here permits the Web Service to maintain state information over all queries made by this client.
// BirthDay bd = new BirthDay() .. must have class level scope if (bd.CookieContainer == null) bd.CookieContainer = new CookieContainer(); IAsyncResult dayOfWeek = bd.BeginGetDay- Born(mo,dy,yr,null,null); Console.WriteLine(bd.EndGetDayBorn(dayOfWeek)); // List all cookies in CookieContainer collection // Create a Uniform Resource Identifier object Uri hostURL = new Uri("http://localhost"); foreach (Cookie ck in bd.CookieContainer.GetCookies(hostURL)) Console.WriteLine(ck.Name+" "+ck.TimeStamp.ToString());
Notice that the CookieContainer
class has a method GetCookies
that returns a collection of cookies stored in the container. In the example, foreach
is used to enumerate the collection and list the name of each cookie along with the date it was created.
We have shown how the proxy code can be generated using wsdl.exe
and compiled into a DLL or used as source code within the client application. If you use VS.NET to create your Web Service client, you have no need for the wsdl.exe
utility. Instead, VS.NET lets you select a Web Service for which you want to generate the proxy code. It adds the code to the project, and you can use the techniques described in the preceding examples to access it. Here are the details:
Open up .NET and select a Windows application.
Select Add Web Reference under the Project menu tab. The subsequent screen contains a text box at the top in which you enter the URL of the Web Service. For example:
http://localhost/ws/BirthDayWS.asmx
Press the Enter key and a screen appears that includes a Web Service Help page for the selected service. Click the Add Reference button and the source code containing the proxy is added to the project.
The proxy code has its own namespace that should be added to the client code. In this example, wsdlclient
is the name of the VS.NET project.
using wsdlclient.localhost;
You will see that a Web References
folder has been added to the directory of the project. To view the proxy source, click the Show All Files buttons in the Solution Explorer and open the Reference.cs
file beneath the localhost
node. Notice that in addition to the proxy code, the directory includes the WSDL file from which the proxy was generated. We'll look at this file in detail in the next section to gain an understanding of how WSDL describes a Web Service. In addition, we will look at how SOAP is used to define the format of messages transported between the Web Service consumer and provider.
The purpose of this section is to add some depth to the topic of Web Services—to look underneath at the XML grammar that is used to describe the service (WSDL) and the protocol (SOAP) used for transporting data between the service and client. Be aware that both of these have been submitted to the World Wide Web Consortium (W3C), but neither is an official standard.
WSDL is broadly defined as “an XML format for describing network services as a set of endpoints operating on messages containing either document-oriented or procedure-oriented information.”[2] In our case, the endpoints are the client and a Web Service, and WSDL defines how the client interacts with the service.
When developing a Web Service from the ground up, it is good practice to develop the interface definition first—in the form of WSDL—and then map it to the implementation code. Although the sheer complexity of WSDL demands that WSDL editors be used for the task, there is still a need for the developer to have a general understanding of the WSDL structure. The same is true even if you work only on the client side and use wsdl.exe
to create source code directly from the WSDL. Problems can arise using utilities and editors that require a familiarity with the format in order to perform troubleshooting. For example, a well-designed WSDL interface is often built from multiple documents tied together by an XML import
element. Being familiar with the semantics and existence of the import
element goes a long way toward solving import issues—a not uncommon source of WSDL errors.
This section introduces the basic structure of WSDL, which should satisfy the curiosity of the occasional Web Service consumer or developer. Those interested in learning more should refer to the specification that is published by W3C at http://www.w3.org/TR/wsdl.
Specifications for the WSDL grammar define six major elements: definitions, types, message, port type, binding, and service. Let's discuss these within the context of the WSDL file that describes the sample BirthDayWS
Web Service.
This is the root element of the WSDL document. It declares multiple namespaces used throughout the document, and contains all of the other elements:
<definitions xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:s0="http://tempuri.org/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" targetNamespace="http://tempuri.org/" xmlns="http://schemas.xmlsoap.org/wsdl/">
Namespaces are used to distinguish elements because it is possible that elements from different namespaces could have the same name.
This element contains an XSD (XML Schema Definition Language) schema that describes the data types publicly exposed by the service: the parameters passed in the Web Service request, and the response:
<types> <s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org/"> <s:element name="GetDayBorn"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="mo" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="day" type="s:int" /> <s:element minOccurs="1" maxOccurs="1" name="yr" type="s:int" /> </s:sequence> </s:complexType> </s:element> <s:element name="GetDayBornResponse"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="GetDayBornResult" type="s:string" /> </s:sequence> </s:complexType> </s:element> </s:schema> </types>
Defines the data that is exchanged between the Web Service provider and consumer. Each message is assigned a unique name and defines its parameters—if any—in terms of names provided by the types
element:
<message name="GetDayBornSoapIn"> <part name="parameters" element="s0:GetDayBorn" /> </message> <message name="GetDayBornSoapOut"> <part name="parameters" element="s0:GetDayBornResponse" /> </message>
Each <portType>
element defines the <Message>
elements that belong to a communications transport. The name
attribute specifies the name for the transport. The <portType>
element contains <operation>
elements that correspond to the methods in the Web Service. The <input>
and <output>
elements define the messages associated with the operation. Four types of operations are supported: one-way, in which the service receives a message; request-response, in which the client sends a request; solicit-response, in which the service first sends a message to the client; and notification, where the service sends a message to clients.
<portType name="BirthDaySoap"> <operation name="GetDayBorn"> <documentation>Return day of week for any date</documentation> <input message="s0:GetDayBornSoapIn" /> <output message="s0:GetDayBornSoapOut" /> </operation> </portType>
A set of rules that describe how the <portType>
operation is transmitted over the wire. Wire protocols available are HTTP GET
, HTTP POST
, and SOAP. This example demonstrates how SOAP is specified.
As acknowledgement of the importance of SOAP as a transport protocol, the WSDL 1.1 specification includes extensions for SOAP 1.1. These extension elements include <binding>
, <operation>
, and <body>
.
<binding name="BirthDaySoap" type="s0:BirthDaySoap"> <soap:binding transport= "http://schemas.xmlsoap.org/soap/http" style="document" /> <operation name="GetDayBorn"> <soap:operation soapAction="http://tempuri.org/GetDayBorn" style="document" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding>
Note that the <operation>
element specifies the entry point for the Web method that is called on the server. One other thing to be aware of is the style
attribute in the binding element. This value, which may be document
or rpc
, specifies how an operation is formatted. By default, .NET sets this value to document
. To specify rpc
, you must apply the SoapRpcMethodAttribute
to the Web method:
[SoapRpcMethod][WebMethod] public string GetDayBorn(string month, int day, int yr)
Although there is a rather spirited debate among WSDL purists as to which is better, you can safely ignore the histrionics and use the .NET default. However, knowing your options will enable you to easily work with third parties that may have a preference.
Identifies the location of the Web Service. Specifically, it lists the name of the Web Service class, the URL, and references the binding for this endpoint.
<service name="BirthDay"> <port name="BirthDaySoap" binding="s0:BirthDaySoap"> <soap:address location= "http://localhost/ws/BirthDayWs.asmx" /> </port> </service>
SOAP is a platform-neutral protocol for exchanging information. Its cross-platform capabilities are attributable to its use of XML to define the data being passed and support for HTTP as a communications protocol. SOAP is the most popular and flexible protocol for the exchange of information between Web Service consumers and providers. Its format allows it to define complex data structures not supported by the competing HTTP GET
and POST
protocols.
Our discussion of SOAP follows the same approach used with WSDL: We examine the basic features of SOAP using the request/response messages generated from
the BirthDayWS
Web Service example. The format of these messages is described on the Web page containing the desired method(s) of the Web Service.
The header for a SOAP request reveals that the SOAP request is packaged as an HTTP POST
request to the server designated by the Host
field. The length
field specifies the number of characters in the body of the POST
, and SOAPAction
indicates the namespace and method to be contacted.
POST /ws/BirthDayWS.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: "http://tempuri.org/GetDayBorn"
Listing 18-3 shows the XML template for the SOAP message that is sent to the server.
Example 18-3. GetDayBorn
SOAP Request Content
<?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:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http:/ /tempuri.org/" xmlns:types="http://tempuri.org/encodedTypes" xmlns:soap="http://schemas .xmlsoap.org/soap/envelope/"> <soap:Body soap:encodingStyle= "http://schemas.xmlsoap.org/soap/encoding/"> <tns:GetDayBorn> <mo xsi:type="xsd:int">int</mo> <day xsi:type="xsd:int">int</day> <yr xsi:type="xsd:int">int</yr> </tns:GetDayBorn> </soap:Body> </soap:Envelope>
The overall structure of a SOAP message is not complex. It is an XML document that has a mandatory root element, <Envelope>
, an optional <Header>
element, and a mandatory <Body>
.
A SOAP envelope, as the name implies, is conceptually a container for the message. The SOAP header represents a way to extend the basic message. It may contain additional information about the message and—as we will see later—can be used to add a measure of security. The SOAP body contains what one would regard as the actual data: the arguments sent to the service and the response. The contents of the <Body>
element in this example consist of the method name and its three parameters that correspond to the call made within the client code:
string dayOfWeek = bd.GetDayBorn(12,20,1963);
The SOAP body of the response includes a <GetDayBornResult>
element (see Listing 18-4) that contains the response from the Web Service and identifies it as a string type.
Example 18-4. GetDayBorn
SOAP Response Content
<?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- enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://tempuri.org/" xmlns:types="http://tem- puri.org/encodedTypes" xmlns:soap="http://schemas.xml- soap.org/soap/envelope/"> <soap:Body soap:encodingStyle= "http://schemas.xmlsoap.org/soap/encoding/"> <tns:GetDayBornResponse> <GetDayBornResult xsi:type="xsd:string">string</GetDayBornResult> </tns:GetDayBornResponse> </soap:Body> </soap:Envelope>
The optional SOAP header is available for adding miscellaneous information about its associated SOAP message. One popular use for this header is to include identification information about the user making the request. This enables user authentication to be performed by the methods within the Web Service.
A question that arises when a Web Service is being invoked from a Web page is whether Forms Authentication (Chapter 17) can be used. The short answer is yes, but it requires a special implementation. Unlike a regular Web page, a Web Service does not recognize the authentication cookie created by the separate login screen. The solution is to add a login method to the Web Services that creates the authentication cookie. In addition, each service must check user authentication before performing its operation. The coding requirements are comparable to using SOAP header authentication.
Listing 18-5 demonstrates a Web Service that checks the header for an ID and password before making the service available to the requestor. It consists of the code taken from Listing 18-1 and updated to include features required to access a SOAP header.
Example 18-5. Authenticate Web Service User by Checking SOAP Header
using System;
using System.Web.Services;
// Required for SoapException
using System.Web.Services.Protocols;
namespace BirthDayWS
{
// (1) Class to hold data passed in the header
public class SOAPHeaderContent : SoapHeader
{
public string UserName;
public string PassWord;
}
public class BirthDay : System.Web.Services.WebService
{
// (2) Member class accessing header data
public SOAPHeaderContent headerInfo;
// (3) Add SoapHeader attribute
[WebMethod(Description="Return day of week for any date"),
SoapHeader("headerInfo")]
public string GetDayBorn(int mo, int day, int yr)
{
if (!Verify())
{
throw new SoapException(
"Valid User info not included.",
SoapException.ClientFaultCode);
} else {
bool err = false;
string dob;
if (mo <1 || mo >12) err=true;
if (day < 1 || day > 31) err=true;
if ( ! err)
{
DateTime dt = new DateTime(yr,mo,day);
dob = dt.ToString("dddd"); // extracts day
} else {
dob = "Invalid Date";
}
return(dob);
}
}
// Method to check password and ID in SOAP header
private bool Verify()
{
// (4) Access data in header
if (headerInfo.UserName != "Vincent" ||
headerInfo.PassWord != "arles")
{ return(false);
} else { return(true);}
}
}
}
This example illustrates the four steps that are followed to receive and access any SOAP header data:
Create a class to represent the data in the header. This class must inherit from the SoapHeader
class. SOAPHeaderContent
serves the purpose in this code.
Add a member to the Web Service class that is the same type as the class created in Step 1. The example uses headerInfo
.
Apply a SoapHeader
attribute that references the member created in Step 2. Applying this to a method makes the information in the header available to the method.
Process the header data by accessing the members of the class created in Step 1. The Verify
method contains the simple logic to check PassWord
and UserName
. In reality, the comparison information would come from a database rather than being hardcoded.
The proxy for this Web Service includes the class that represents the header contents. The client creates an instance of this class and assigns a password and user name. The class instance is assigned to a field that is now a member of the proxy class representing the Web Service class. In this example, Birthday
now has a field named SOAPHeaderContentValue
. As you have probably guessed, .NET creates this field name by appending Value
to the name of the class that accesses the header info (SOAPHeader
).
using System;
using System.Web.Services;
using system.Web.Services.Protocols;
using System.Text.RegularExpressions;
public class BirthDayClient
{
static void Main(string[] args)
{
SOAPHeaderContent acctInfo = new SOAPHeaderContent();
acctInfo.UserName = "Vincent";
acctInfo.PassWord = "arles";
BirthDay bd = new BirthDay();
bd.SOAPHeaderContentValue = acctInfo;
try {
string dayOfWeek = bd.GetDayBorn(12,20,1963);
Console.WriteLine(dayOfWeek);
} catch (SoapException ex)
{
// Extract Soap error message
// Be sure to add:
// using System.Text.RegularExperssions
Match errMatch = Regex.Match(ex.Message,":(.*)");
Console.WriteLine(errMatch.Groups[1]);
}
}
}
As shown in Listing 18-5, the Web Service throws an exception if the user name and password cannot be authenticated:
throw new SoapException("Valid User info not included.",
SoapException.ClientFaultCode);
The exception is rendered as a SoapException
object. In this example, its constructor receives a message and a SOAP fault code that signifies the type of error. This information is returned to the client as a <Fault>
element in the SOAP body and is converted by the .NET Framework back to a SoapException
object that can be processed by the client's code.
The SoapException
object includes four properties that provide information about the exception:
Message
. The error message that explains the reason for the exception.
Actor
. The URL of the Web Service that threw the exception.
Code
. An XMLQualifiedName
object that specifies one of four SOAP fault codes that categorize the exception. These fault codes are represented as static fields of the SoapException
class.
Detail
. An XMLNode
object containing application-specific information about the error.
The Message
and Code
properties are used most frequently in processing a SOAP exception. The message is verbose: It consists of the full namespace qualification of the SoapException
class followed by the actual message and the name of the Web method where the exception occurred. A regex was used in the preceding client code to extract the actual message.
The SoapException
class contains static fields that can be compared with the Code
value to broadly classify the exception. These fields include the following:
VersionMismatchFaultCode
. The SOAP envelope has an invalid namespace.
ClientFaultCode
. The message sent by the client is incorrectly formatted or contains incorrect information.
ServerFaultCode
. The error occurs on the server and is not related to the SOAP message. An example would be a network or hardware problem.
MustUnderstandFaultCode
. A SOAP element marked with the MustUnderstand
attribute set to true
is not processed. This is an attribute that indicates the particular element must be processed.
Here is a code sample that demonstrates using the Code
property. It first extracts a message embedded in the Message
property and prints it. Then it checks the Code
property to classify the excepton.
try { string dayOfWeek = bd.GetDayBorn(12,20,1963); Console.WriteLine(dayOfWeek); } catch (SoapException ex) { Match errMatch = Regex.Match(ex.Message,":(.*)"); Console.WriteLine(errMatch.Groups[1]); // Check various fault codes here if(ex.Code == SoapException.ClientFaultCode) { Console.WriteLine("Problem with Client message."); } if(ex.Code == SoapException.ServerFaultCode) { Console.WriteLine("Problem with Server. Try again."); } }
The preceding example illustrates a lightweight technique for using SOAP headers to authenticate a user. Its main drawback is that the contents of the SOAP headers are passed as cleartext. To overcome this, you can add a layer of encryption by using this technique in conjunction with Secure Sockets Layer (SSL).
As an alternative to this and other ad hoc approaches to SOAP security, there is now a Web Services Security (WS-Security) specification [3] that defines enhancements to SOAP messaging. Its objectives are to ensure the following:
Authentication. How a SOAP message expresses the identity of its sender.
Integrity. How to verify that a SOAP message has not been tampered with.
Confidentiality. What protects the contents of a SOAP message from being read by an intermediary.
In support of this specification, Microsoft provides an add-on to the .NET Framework known as Web Services Enhancements 2.0 (WSE 2.0). This tool set comprises a .NET class library that can be used to integrate the WS-Security specification into Web Service applications. Note that the use of these enhancements requires .NET on both the client and service provider endpoints, so it's not yet a generic Web Service security solution. See the MSDN Web site, msdn.microsoft.com/webservices/webservices/building/wse/default.aspx, for whitepapers and a free download of the enhancements library
The BirthDayWS
Web Service used throughout this chapter accepts integers as input and returns a string
value. This is useful for introducing Web Service principles because HTTP GET
, HTTP POST
, and SOAP all support it. However, Web Services also have the capability of serving up more complex data types such as data sets, hash tables, images, and custom objects.
Before data can be sent to or from a Web Service, it is serialized using XML serialization. Conversely, it is deserialized on the receiving end so it can be restored to its original type. As we saw in Chapter 4, “Working with Objects in C#,” not all data can be serialized. Thus, when designing a Web Service, it is important to understand restrictions that apply to serialization:
XML serialization can only be used with classes that contain a public parameterless constructor. For example, you may have a Web Service that returns a hash table because it has the constructor public Hashtable()
. On the other hand, the Bitmap
class does not have a parameterless constructor and cannot be used as a return type.
Read-only properties in a class cannot be serialized. The property must have a get
and set
accessor and be public.
Fields must be public to be serialized; private ones are ignored.
In this section, we work with two Web Service examples that illustrate the use of complex data. Our first example creates a Web Service that accepts the name of an image and returns it as a byte stream. The second example creates a client to use the Amazon Web Services provided by Amazon.com, Inc. These services offer a rich—but practical—sampling of accessing multiple Web methods and processing a wide variety of custom classes.
Image manipulation usually requires representing an image as a Bitmap
object. However, because bitmaps cannot be serialized and transferred directly, we must find an indirect way to transport an image. The not-so-difficult solution is to break the image into bytes and return a byte stream to the client, who is responsible for transforming the stream to an image.
The logic on the server side is straightforward: a FileStream
is opened and associated with the image file. Its contents are read into memory and converted to a byte array using
tempStream.ToArray()
This byte array is then sent to the Web client (see Listing 18-6).
Example 18-6. Web Service to Return an Image as a String of Bytes
<%@ WebService Language="C#" Class="WSImages" %>
using System;
using System.Web.Services;
using System.IO;
using System.Web.Services.Protocols;
public class WSImages: System.Web.Services.WebService {
[WebMethod(Description="Request an Image")]
public byte[] GetImage(string imgName) {
byte[] imgArray;
imgArray = getBinaryFile("c:\"+imgName+".gif");
if (imgArray.Length <2)
{
throw new SoapException(
"Could not open image on server.",
SoapException.ServerFaultCode);
} else
{
return(imgArray);
}
}
public byte[] getBinaryFile(string filename)
{
if(File.Exists(filename)) {
try {
FileStream s = File.OpenRead(filename);
return ConvertStreamToByteBuffer(s);
}
catch(Exception e)
{
return new byte[0];
}
} else { return new byte[0]; }
}
// Write image to memory as a stream of bytes
public byte[] ConvertStreamToByteBuffer(Stream imgStream) {
int imgByte;
MemoryStream tempStream = new MemoryStream();
while((imgByte=imgStream.ReadByte())!=-1) {
tempStream.WriteByte(((byte)imgByte));
}
return tempStream.ToArray(); // Convert to array of bytes
}
}
Our client code receives the byte stream representing an image and reassembles it into a Bitmap
object. Because the Bitmap
constructor accepts a stream type, we convert the byte array to a MemoryStream
and pass it to the constructor. It can now be manipulated as an image.
WSImages myImage = new WSImages();
try {
// Request an image from the Web Service
byte[] image = myImage.GetImage("stanwyck");
MemoryStream memStream = new MemoryStream(image);
Console.WriteLine(memStream.Length);
// Convert memory stream to a Bitmap
Bitmap bm = new Bitmap(memStream);
// Save image returned to local disk
bm.Save("c:\bstanwyck.jpg",
System.Drawing.Imaging.ImageFormat.Jpeg);
}
catch (WebException ex)
{
Console.WriteLine(ex.Message);
}
To use the Amazon E-Commerce Service, you must register for a developer's token, which is required as part of all requests made to the Web Services. In addition, you should download (http://www.amazon.com/webservices) the developer's kit that contains the latest documentation, examples, and—most importantly—a WSDL file defining all the services.
An examination of the WSDL file reveals that AmazonSearchService
is the Web Service class that contains the numerous search methods available to clients. These methods provide the ability to search the Amazon product database by keyword, author, artist, ISBN number, manufacturer, actor, and a number of other criteria. Each search method takes a search object as an argument that describes the request to the server and returns a ProductInfo
object. For example, a request to search by keywords looks like this:
AmazonSearchService amazon = new AmazonSearchService(); KeywordRequest kwRequest = new KeywordRequest(); // Set fields for kwRequest ProductInfo products = amazon.KeywordSearchRequest(kwRequest);
Table 18-3 contains a sampling of the methods available for searching Amazon products. These methods are for accessing the Web Service synchronously. An asynchronous form of each method is also available that can be accessed using the techniques discussed earlier in this chapter.
Table 18-3. Selected Methods of AmazonSearchService
Class
Method | Description |
---|---|
ProductInfo KeyWordSearchRequest (KeywordRequest req) | Method to return items that contain one or more keywords provided in request. |
ProductInfo AsinSearchRequest (AsinRequest req) | Method to return a book having a requested Amazon Standard Identification Number (ASIN) that is the same as the book's ISBN. Represented as a 10-digit string. |
ProductInfo AuthorSearchRequest (AuthorRequest req) | Method to return names of all books by requested author. |
ProductInfo ActorSearchRequest (ActorRequest req) | Method to return video titles of movies in which a specified actor or actress was a cast member. |
ProductInfo PowerSearchRequest (PowerRequest req) | Method to retrieve book information based on a Boolean query that may include a combination of title, subject, author, keyword, ISBN, publisher, language, and publication date (pubdate). |
Each call to a Web method passes an object that describes the search request. This object is different for each method—for example, AuthorSearchRequest
requires an AuthorRequest
object, whereas KeyWordSearchRequest
requires an instance of the KeywordRequest
class. These classes expose almost identical fields. Each contains a unique string field that represents the search query, five other required fields common to each class, and some optional fields for sorting or specifying a locale. Table 18-4 lists unique and shared fields for each method listed in Table 18-3.
Table 18-4. Selected Fields for Classes That Define a Search Request
Field | Description |
---|---|
KeywordRequest.Keyword AsinRequest.Asin ActorRequest.Actor AuthorRequest.Author PowerRequest.Power | These are string values containing the search value or query. For example: PowerRequest pr = new PowerRequest(); pr.Power = "author:Nabokov and keyword:butterfly"; |
| Page of results to display. |
| Type of products being searched—for example, |
| Amazon associate's ID. Use |
|
|
| Developer's token assigned to you by Amazon. |
The Web Service responds to the search request with a ProductInfo
object containing results from the search. This object exposes three important fields: a TotalResult
s string contains the number of products retrieved by the request, a TotalPages
string that indicates how many pages these results are displayed in, and the important Details
array that contains a detailed description of products that constitute one returned page of results. This array is of the Details
type. Table 18-5 shows the fields that are related to books.
Table 18-5. Selected Fields of the Details
Class
Field | Description |
---|---|
| Name of a single product. |
| Ranking of product based on sales of items of its type. |
| Publisher of book. |
String ListPrice string OurPrice | List and sales price of book. |
| The reviews of the book: string AvgCustomerRating string TotalCustomerReviews CustomerReview CustomerReviews string Comment string Rating |
| One or more authors for the book. |
This is only a small sample of the fields available in the Details
class. There is a particularly rich set of fields worth exploring that define video products.
Our first step is to create a proxy class from the Amazon WSDL information. The downloadable kit includes a WSDL file, and it can also be retrieved from the Internet, as we do here. Using the VS.NET command line, we place the proxy source code in the file AZProxy.cs
.
wsdl.exe /out:c:clientAZProxy.cs http://soap.amazon.com/schema3/AmazonWebServices.wsdl
Next, we create an assembly, AZProxy.dll
, containing the proxy that will be used by client code. It is linked to assemblies containing .NET Web classes required by the application.
csc/t:library /r:system.web.services.dll /r:system.xml.dll AZProxy.cs
You can make a quick test of the service using this barebones application, azclient.cs
:
using System; using System.Web.Services; namespace webclient.example { public class AmazonClient { static void Main(string[] args) { // Search for books matching keyword "butterflies" AmazonSearchService amazon = new AmazonSearchService(); KeywordRequest kwRequest = new KeywordRequest(); kwRequest.keyword = "butterflies"; kwRequest.type = "heavy"; kwRequest.devtag= "*************"; // your developer token kwRequest.mode = "books"; // search books only kwRequest.tag = "webservices-20"; kwRequest.page = "1"; // return first page ProductInfo products = amazon.KeywordSearchRequest(kwRequest); Console.WriteLine(products.TotalResults); // Results count } } }
Compile and execute this from the command line:
csc /r:AZProxy.dll azclient.cs azclient
When azclient.exe
is executed, it should print the number of matching results.
Let's design a Windows Forms application that permits a user to perform searches on books using multiple search options. Open VS.NET and select a Windows application. After this is open, we need to add a reference to the proxy assembly AZProxy.dll
. From the menu, select Project – Add Reference – Browse. Click the assembly when it is located and then click OK to add it as a reference. You also need to add a reference to System.Web.Services.dll
, which contains the required Web Service namespaces.
The purpose of the application is to permit a user to search the Amazon book database by keyword, author, or title. The search can be on a single field on a combination of fields. Figure 18-7 shows the interface for entering the search values and viewing the results. The Search buttons submit a search request based on the value in their corresponding text box. The Power Search button creates a query that logically “ands” any values in the text boxes and submits it.
A single page of results is displayed in a ListView
control. Beneath the control are buttons that can be used to navigate backward and forward through the results pages.
Each Search button has a Click
event handler that calls a method to create an appropriate request object and send it to the Amazon Web Service. A successful call returns a ProductInfo
object containing information about up to 10 books meeting the search criteria. Listing 18-7 displays code that creates an AuthorRequest
object, sends it to the Web Service, and calls FillListView
to display the results in the ListView
control.
Example 18-7. Client Code to Display Results of Author Search—azwsclient.cs
// Fields having class-wide scope
int CurrPg; // Current page being displayed
string SearchMode = ""; // Current search mode
int MaxPages =1; // Number of pages available
// This method is called when author Search button is clicked
private bool AuthorReq()
{
AmazonSearchService amazon = new AmazonSearchService();
AuthorRequest auRequest = new AuthorRequest();
auRequest.author = textBox2.Text; // Get author from GUI
auRequest.type = "heavy";
auRequest.devtag= "****KLMJFLGV9"; // Developer token
auRequest.mode = "books";
auRequest.tag = "webservices-20";
auRequest.page = CurrPg.ToString();
try
{
// Call Web Service with author query
ProductInfo products =
amazon.AuthorSearchRequest(auRequest);
FillListView(products);
return(true);
}
catch (SoapException ex)
{
MessageBox.Show(ex.Message);
return(false);
}
}
private void FillListView(ProductInfo products)
listView1.Items.Clear(); // Remove current entries
label6.Text=""; // Clear any title
label4.Text = products.TotalResults;
label5.Text = CurrPg.ToString()+" of "+products.TotalPages;
{
MaxPages = Convert.ToInt32(products.TotalPages);
ListViewItem rowItem;
string auth,rev;
for (int i=0; i< products.Details.Length; i++)
{
rowItem = new
ListViewItem(products.Details[i].ProductName);
// Add Author. Make sure author exists.
object ob = products.Details[i].Authors;
if (ob != null) auth =
products.Details[i].Authors[0]; else auth="None";
rowItem.SubItems.Add(auth);
// Add Price
rowItem.SubItems.Add(products.Details[i].OurPrice);
// Add Average Rating
ob = products.Details[i].Reviews;
if (ob != null) rev =
products.Details[i].Reviews.AvgCustomerRating;
else rev="None";
rowItem.SubItems.Add(rev);
// Add Date Published
rowItem.SubItems.Add(
products.Details[i].ReleaseDate);
listView1.Items.Add(rowItem);
}
}
The keyword, title, and power searches use an identical approach: Each has a routine comparable to AuthorReq
that creates its own request object. The only significant difference pertains to the power search that creates a Boolean query from the search field values. The format for this type query is field:value AND field2:value AND field3:value.
For example:
"author:hemingway AND keywords:Kilimanjaro"
This application was designed as a Windows Forms application. It could just as easily be set up as Web page under ASP.NET. The code to reference the assembly AZProxy.dll
is identical. The ListView
control is not supported on a Web Form, but you could easily substitute a DataGrid
for it.
The performance of a Web Service from both the client and server side is affected by a variety of factors. Some are .NET related and others are inherent in the technology. The solutions for improving Web Services performance range from shaving a few milliseconds off the way .NET sends a request to simply eschewing Web Services for an alternate protocol when transferring large amounts of data. We'll look at all of these.
Connections (HTTP) to Internet resources are managed in .NET by the ServicePoint
class. This class includes properties to specify a connection's timeout interval, set its security protocol, and manage the use of server security certificates. It also includes properties that directly affect how much delay is incurred before a Web Service request is transmitted over a network: UseNagleAlgorithm
and Expect100Continue
. Despite the awkward sounding names, setting their properties is as easy as assigning a true
or false
value to them. The default for both is true
.
This property determines whether a POST
request should expect to receive a 100-Continue response from the server to indicate that the data can be posted. If this value is set to true
, only the request header portion of a request is sent to the server. If the server finds no problem with the header, it returns a 100-Continue response, and the data is then sent. Two trips are required. If the property is set to false
, the initial request includes the headers and data. If it is rejected by the server, the data has been sent unnecessarily; if it is accepted, a second trip is not necessary.
Because Web Service calls tend to pass small amounts of data, it can be beneficial to turn this property off. Even if a request is rejected, only a small amount of data will have been sent.
One way to improve network efficiency is to reduce the number of small data packets sent across a network. To accomplish this, the software layer controlling the underlying TCP (Transmission Control Protocol) connection attempts to accumulate, or buffer, small messages into a larger TCP segment before they are sent. The technique to do this is based on the Nagle algorithm.[4]
The crux of the algorithm is that small amounts of data should continue to be collected by TCP until it receives acknowledgment to send the data. .NET institutes a delay of up to 200 milliseconds to collect additional data for a packet. For a typically small Web Service request, there may be no reason to include this delay. It's an option you can experiment with.
To set the Expect100Continue
and UseNagleAlgorithm
properties, it is necessary to get a reference to the ServicePoint
object being used to handle the Web request. This is done in the proxy code on the client side. Refer to Listing 18-2, and you'll see that the proxy code consists of a class derived from the base SoapHttpClientProtocol
class. By overriding the inherited GetWebRequest
method, you can customize the WebRequest
object before the request is sent to the Web Service.
Add the following code to override the GetWebRequest
method. Inside the method, you use the Uri
object to get the ServicePoint
. Its properties can then be turned off or on to test performance:
// Using System.Net must be added protected override WebRequest GetWebRequest( Uri uri) { // Locate ServicePoint object used by this application ServicePoint sp = ServicePointManager.FindServicePoint(uri); sp.Expect100Continue = false; sp.UseNagleAlgorithm = false; return WebRequest.Create(uri); }
Although the XML format offers a great deal of flexibility in representing data, it can place a potentially large burden on a network because of the large files that can be generated. Moreover, processing XML data can require extensive memory resources on the client and server. Finally, there is the nature of a Web Service: If the transmission fails at any point, the entire response must be resent. This is in contrast to FTP and the HTTP GET
verb that allow partial data transmission.
For large amounts of data, consider these options:
Use FTP or a Web client as described in Section 17.6, “Creating a Web Client with WebRequest and WebResponse.”
Avoid calls as much as possible by caching data rather re-requesting it.
Look at compression techniques such as HTTP transport compression or the SOAP extensions that can compress part of a Web Service message.
Wait for new Web Service standards. Of particular note is Message Transmission Optimization Mechanism (MTOM), a new W3C recommendation—pushed by Microsoft—that details a method for attaching large binary data to a SOAP message.
.NET supports the development of both Web Service provider and Web Service consumer applications. On the server side, a .NET Web Service is easily constructed using a WebService
directive to define the Web Service class and a WebMethod
attribute to identify methods accessible to HTTP requests. For the Web Service to be used by clients, a description of the service(s) must be made available. This is the purpose of the Web Service Description Language (WSDL) contract. This XML file describes the service, the methods available, and a description of the arguments each method accepts. You can generate the contract by using a browser to navigate to a URL address that consists of the URL of the Web Service with ?WSDL
appended to it.
The WSDL information is used on the client side to create a Proxy class that is used to actually communicate with the Web Service. This proxy is created using the wsdl.exe
utility or within a Visual Studio.NET project as part of the Add Web Service option. The proxy defines methods that permit the Web Service methods to be accessed synchronously or asynchronously. The latter technique returns control to the application while the request is being processed.
SOAP (Simple Object Access Protocol) describes the XML format that is used to transport information between a Web Service provider and consumer. Its structure consists of an envelope, optional header, and body. The body contains the actual data or message; the header may contain annotation about the message. One such use is to include user authentication information. The main advantage of SOAP over the other two wire protocols—HTTP GET
and HTTP POST
—is that it supports the transmission of non-text data such as images and objects. We demonstrated this by building a client to access the Amazon Web Services.