Working with Web Services

Overview

One thing life has taught me: if you are interested, you never have to look for new interests. They come to you. When you are genuinely interested in one thing, it will always lead to something else.

-Eleanor Roosevelt

Web services (sometimes called XML Web services) are software components that provide some type of service over the Internet. Just like a conventional Web site, a Web service is reachable through a public URL and is subject to the same security restrictions of an HTML-based Web site. Unlike ordinary Web sites, though, Web services don't have a user interface. Web services are programmable entities that expose callable functions.

The caller can certainly be a human, but it will more likely be an application. From a developer's perspective, a Web service is a sort of platform-independent dynamic-link library (DLL). A Web service is hosted by a Web site and exposes a number of functions with well-known signatures. These exposed functions are known as Web methods. By sending proper HTTP requests, and as long as the security layer of the site lets your request pass, you execute Web methods and get results back.

A number of standards are involved with Web services, the first in line being an Internet protocol, most commonly HTTP. The presence of such a ubiquitous transportation protocol is the key element that guarantees the platform-independence of Web services. Note that, architecturally speaking, Web services are not strictly tied to HTTP as the protocol. Web services can be devised and implemented on top of a number of transportation protocols, including SMTP, FTP, and message queues. HTTP is the most popular of all and the only one supported by the .NET Framework. For this reason, it's unlikely that you'll ever use another one even though other protocols could be used. Other fundamental standards in the Web service infrastructure are XML and the Simple Object Access Protocol (SOAP). XML comes up as the universal language to describe the data being exchanged. SOAP is an XML dialect and provides a universal syntax to define a Web method call. Any data returned by a Web service is defined by an XML schema, which makes it consumable on a large number of platforms. The availability of HTTP listeners and XML parsers on virtually any current software platform gives Web services tremendous implementation and deployment power. A Web service can be installed on any platform and be accessed from any other platform. These days, as a human user, you'll experience no difficulty at all connecting to a Web site hosted on a Linux-based machine. Likewise, for your client software, calling a Web service hosted on a Linux machine is no big deal, as long as a public URL and a public Web method signature are provided.

Web services are not an exclusive feature of the .NET platform. The .NET platform was one of the first platforms to provide programmers with significant and powerful support to call and create Web services. ASP.NET applications are just one possible client of Web services, along with Windows Forms, consoles, and even mobile and smart-device applications. The ASP.NET runtime is deeply involved in the implementation of Web services on the .NET platform. As we'll see later, .NET Web services are just a special case of an ASP.NET application.

In this chapter, we'll cover the .NET infrastructure that allows the creation of .NET Web services, how to build Web services, and how to call them. In doing so, we'll touch on critical aspects of Web programming such as security and state management.

The Infrastructure for Web Services

The infrastructure of Web services embraces some open Internet standards, such as HTTP, XML, SOAP, and the Web Services Description Language (WSDL). The combined use of such open standards makes Web services accessible and consumable from any client or Internet-enabled device. We know about HTTP from discussions in earlier chapters and we discussed XML in Chapter 16, so let's start by taking a quick tour of SOAP and WSDL.

SOAP provides the official XML-based syntax to be used for exchanging structured and typed information between peers in a distributed environment. The SOAP syntax defines how the client of the Web service can specify the method to execute, related parameters, and how the service will return the data. The Web service resides on a Web server, which recognizes and understands SOAP packets.

WSDL is the XML-based language that describes a Web service. In particular, a WSDL document indicates the location of the service and the methods the service exposes with their parameters. The WSDL document represents the type library of the Web service and is sometimes useful to clients for understanding how to call a given service.

  Note

This book does not cover SOAP and WSDL in detail. While this chapter might be enough to refresh concepts you've already learned or to get you started, I doubt that it will give you a comprehensive understanding of the standards. I suggest you read more specific books such as SOAP: Cross Platform Web Services Development Using XML by Scott Seely and Kent Sharkey (Prentice Hall, 2001) or Building XML Web Services for the Microsoft .NET Platform by Scott Short (Microsoft Press, 2002).

Figure 17-1 shows how the various Internet open standards are related to each other in a Web service architecture.

Figure 17-1: The Web service architecture and the role of the Internet open standards.

The SOAP Protocol

A good definition for the SOAP protocol is the following: SOAP is an XML-based protocol for invoking methods on a remote server component. So SOAP is an encoding mechanism that combined with a transport protocol links nodes of a distributed environment. Unlike other remoting protocols such as Distributed COM (DCOM), Java Remote Method Invocation (RMI), and the CORBA Internet InterORB Protocol (IIOP), SOAP is more lightweight. More exactly, SOAP is merely a wire protocol and would need a distributed environment built around it to be fully comparable with DCOM or RMI.

The inherent simplicity of SOAP doesn't equate to a lack of key features, however. SOAP is designed to provide developers with the most frequently used set of functionality, using a general-purpose protocol meant to extend current Internet protocols. The goal of SOAP is invoking remote methods. What is really needed to do this? We simply need a protocol capable of serializing a method call in a format that is transportable and recognizable on a variety of platforms. In addition, we need a transportation protocol to deliver method calls and return results. This is the essence of SOAP.

The SOAP Payload

SOAP commonly uses HTTP as the transportation protocol. This means that invoking a method on a Web service is a matter of sending a SOAP-formatted XML to a Web service using the HTTP POST command. The following code snippet shows a typical SOAP request:

POST /samples/services/MathService/MathService.asmx HTTP/1.1 Host: samples.gotdotnet.com Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: "http://tempuri.org/Multiply" 2 3

In particular, the request is directed at a Web service named MathService.asmx and located on the host http://samples.gotdotnet.com in a given path. The method to invoke is Multiply, and the parameters are two float numbers, such as 2 and 3. As you can see, the Web service provides some rather simple mathematical services. The script just shown requests that the Web service executes 2 times 3 and returns the value. The computed value of the operation is returned to the client in a response packet like the following one:

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

Let's examine more closely the format of the request/response packets. A SOAP request is made of an HTTP header, a SOAP action, a blank line, and a SOAP envelope.

The body of the envelope contains a child node whose name is XXXResponse, where XXX is the name of the method. The node contains a child element named XXXResult, where again XXX stands for the actual name of the method invoked.

  Note

The SOAP data model relies on the XML Schema type system. You can use a number of primitive types, including String, Char, Byte, Boolean, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single, Double, Guid, Decimal, and DateTime. In addition, SOAP supports enumeration types, class and struct types with public properties, and arrays of the above. Complex data is serialized to XML text by using a combination of elements and attributes.

Architectural Issues

Because SOAP is intended to be a simple wire protocol, important features that would normally be found in other distributed architectures are unspecified in SOAP. While SOAP doesn't preclude supporting these features—such as security, object activation, state management, and garbage collection—neither does SOAP have built-in mechanisms for them. Some higher-level system must provide these features, using SOAP as the means to transport the underlying data to and from client and server. This is a fundamental difference between SOAP and fully featured distributed environments such as DCOM, CORBA, or RMI. SOAP carries the information—it doesn't process it.

SOAP specifically declines any liability for security aspects. SOAP does not provide its own security layer, but it can work with any infrastructure you provide. For example, you can send SOAP packets over secure sockets or HTTPS. The Web Services Enhancements for Microsoft .NET (WSE) kit also allows developers to incorporate security, routing, and attachment features in their .NET Framework applications. See the "Resources" section at the end of the chapter for more information.

Likewise, the activation of the remote object to serve the call is left to the server-side infrastructure. How and when the object is instantiated, how its memory is managed, and how its life cycle is monitored are all issues that are deliberately left uncovered in the SOAP specification.

Finally, because SOAP works mostly on top of HTTP, which is a stateless protocol, it doesn't supply any facility for maintaining the state of an object across invocations. This is still possible, but implementation details are left to the distributed model in use.

To learn more about the SOAP specification, take a look at the W3C Web site at http://www.w3.org/TR/soap.

The WSDL Language

WSDL is a specification to describe the structure and features of Web services. By looking at the WSDL description of a Web service, a potential client learns about the exposed methods, their signatures, and their return values. WSDL is a key part of the Universal Description, Discovery, and Integration (UDDI) initiative aimed at building a public, cross-platform directory of available Web services.

WSDL defines an XML grammar to describe Web services as collections of network endpoints, or ports, capable of exchanging messages. In WSDL, endpoints and messages are separated from any concrete reference to protocols and data formats. As a result, the specification makes intensive use of a few general terms (for example, messages, ports, bindings, and operations) to abstract what in practice becomes a Web service method call operated through SOAP and HTTP.

Elements of the WSDL Syntax

To describe a network service, a WSDL document uses the elements listed in Table 17-1.

Table 17-1: Abstract Elements of a WSDL Document

Element

Description

binding

Represents a concrete protocol and data format specification for a particular port type

message

Is an abstract definition of the data being exchanged, and is used to represent the input and output of a method

operation

Is an abstract description of an action supported by the service, and is used to represent a callable method

port

Is defined as a combination of a binding and a URL, and represents the callable Web service

portType

Represents an abstract set of operations supported by one or more services defined in the document

service

Is a collection of related endpoints that indicates how many and which Web services have been found at the specified URL

types

Is a container for data type definitions expressed using XSD

To better understand the relationship and the role of these abstract elements, take a look at Figure 17-2.

Figure 17-2: A hierarchical representation of the syntactical elements of a WSDL document.

The logical root of a WSDL document is the tag, which lists all the callable endpoints found at the processed URL. You find an endpoint for each possible way of calling the Web service—typically through SOAP packets or direct HTTP-POST or HTTP-GET commands. In this case, three elements will be found, each pointing to the physical URL to use.

Each Web service can have one or more bindings. You should think of a binding as a possible way to consume a Web service. As mentioned previously, you can consume a Web service by using various protocols. Each of them requires a binding to specify details. For example, the binding to the HTTP-POST protocol will explicitly indicate that the MIME content type of the input parameter is application/x-www-form-urlencoded and the verb to use is POST.

Each node refers to a port type to list all operations supported by the Web service. At this level, the operations are described in a rather abstract way, using messages instead of information about input and output parameters. In this context, a port type resembles an interface—an abstract set of logically related methods. Finally, any message points directly or indirectly to a name for the formal parameter and a type definition. A port type bound to SOAP will use a type definition within the block. A port type bound to HTTP-POST or HTTP-GET will define the name and type within the element.

A Sample WSDL Document

Let's consider a sample WSDL document; admittedly not a really complex one. However, once you understand this document, you'll be ready to tackle others with much more articulated structures. Consider the sample Web service available at http://apps.gotdotnet.com/QuickStart/HelloWorld/HelloWorld.asmx. It's made of a single method named SayHelloWorld having the following signature:

public string SayHelloWorld()

The WSDL for such a Web service is as follows:

Looking at Figure 17-3, you can easily find a match between the abstract structure of Figure 17-2 and the listing just shown.

Figure 17-3: Mapping the actual nodes of the sample WSDL document to the abstract structure.

If you still find the WSDL language intimidating, don't worry. It's very unlikely that you'll have to create or consume WSDL on your own. Web Service platforms today generally have handy tools, system facilities, or utilities to consume and process WSDL automatically for you.

  Note

To get the WSDL for a Web service based on the .NET Framework, you can invoke the URL of the Web service from your browser by appending ?wsdl to the query string.

The NET Infrastructure for Web Services

Historically speaking, Web services and the .NET Framework were introduced at roughly the same time in the summer of 2000, when SOAP was already taking root in the industry. Although it might be easy to associate Web services with the .NET platform, there is no strict dependency between them. As mentioned earlier, the .NET Framework is simply one of the platforms that support Web services—probably the first to integrate Web services in the framework from the ground up.

These days all major software vendors are rapidly transforming the raw idea of software callable by other software into a feature that fits seamlessly into the respective development platforms. A Web service can be created in a platform-specific manner, but the way in which it's exposed to the public is standard and universal. Let's examine the support that the .NET platform provides for building and running Web services.

IIS Support

Because a Web service is accessible through a public URL, it requires some sort of support from the Web server. In particular, the Web server must include a module capable of handling and parsing SOAP packets. The way in which this module gets into the game might vary with different Web servers. However, the availability of a SOAP parser within a Web server is a must for any server platform (for example, IIS or Apache) that wants to support Web services.

In .NET with IIS acting as the Web server, a Web service is an ASP.NET application saved with an .asmx extension. As Figure 17-4 demonstrates, IIS is configured to pass any requests for .asmx resources to the same ISAPI module that processes .aspx files. Instructions to handle Web services are stored in the IIS metabase during the installation of the .NET Framework.

Figure 17-4: The IIS mapping between .asmx files and the ASP.NET runtime module.

  Tip

For this reason, it's recommended that you install the .NET Framework after installing IIS. If you install IIS at a later time, you should run a utility to ensure that all configuration work is done. The utility is aspnet_regiis.exe, and you can find it in the C:…Microsoft.NETFrameworkv1.x.xxxx directory. The tool supports a variety of arguments on the command line. Use the following syntax:

aspnet_regiis.exe -i

Calls for Web services always come through port 80 as conventional HTTP requests. IIS intercepts these calls, looks at the extension of the resource, and hands the request over to the registered handler—the ASP.NET runtime. What happens next is described in Chapter 2 and is nearly identical to the request of a Web page. The ASP.NET worker process receives the request and routes it to an HTTP handler module specialized to handle Web service calls. The class is named WebServiceHandler and is defined within the System.Web.Services.Protocols namespace. The architecture is shown in Figure 17-5.

Figure 17-5: The ASP.NET architecture to process page and Web service requests.

Web Services Hosted by Apache Servers

In the Apache world (see http://ws.apache.org), Axis is the cutting-edge implementation of the SOAP specification. Based on Java, Axis requires the use of a servlet engine such as Tomcat (see http://jakarta.apache.org/tomcat ). Once installed on the Web server machine, Axis is the server-side handler for Web service calls. Axis is a SOAP implementation that can host your own Web services but can also be added to your own Web application to expose it as a Web service.

Generally speaking, an Axis Web service consists of a plain Java class visible in the Web application space. You'll use an XML configuration file to specify which methods you want to expose, which security constraints are needed, and other useful information. A simpler alternative is renaming the Java class that represents the Web service, giving it a .jws extension, and putting it in a publicly accessible Web folder. When someone asks for the .jws resource, the Web server hands the request out to Axis. The class is then compiled and executed (much like a Java Server Page), exposing its public methods as Web methods.

The WebService Class

In the .NET Framework, a Web service is an ordinary class with public and protected methods. Web service files contain a directive that informs the ASP.NET runtime about the nature of the file, the language in use throughout, and the main class that implements the service:

<%@ WebService Language="C#" %>

The main class must match the name declared in the Class attribute and must be public:

public class MyWebService : WebService { }

Indicating the base class for a .NET Web service is not mandatory. A Web service can also be architected starting from the ground up by using a new class. Inheriting the behavior of the WebService class has some advantages, though. A Web service based on the System.Web.Services.WebService class gets direct access to common ASP.NET objects, including Application, Request, Cache, Session, and Server. These objects are packed into an HttpContext object, which also includes the time when the request was made. By using the WebService base class, a Web service also sees the ASP.NET server User object, which under certain conditions can be employed to verify the credentials of the user (often the anonymous user) who is executing the methods. If you don't have any need to access the ASP.NET object model, you can do without the WebService class and simply implement the Web service as a class with public methods.

  Note

Even if you don't inherit the Web service from the WebService class, you can still access ASP.NET intrinsic objects by using the HttpContext.Current static property, which gets the HttpContext object for the current HTTP request. The HttpContext class is defined in the System.Web namespace.

Building an ASP NET Web Service

So you want to build your first Web service. Where do you start? Within the .NET Framework, a Web service is simply a class that can optionally inherit from WebService. As such, the class can implement any number of interfaces and can inherit from other user-defined classes. Moreover, when designing a real-world Web service, you're encouraged to employ standard object-oriented programming practices—such as inheritance and polymorphism—to reuse code and simplify the design. If you proceed this way, you derive the root class of the hierarchy from WebService and have the base class behavior inherited by all actual services you build.

Writing a Web Service Class

The Web service class is normally coded in an .asmx file. The file is made available to potential clients through a Web server virtual directory and is actually reached using a URL. Any client that can issue HTTP commands can actually connect to the Web service.

Let's create a Web service that accesses the Northwind database and provides information about customers and orders. The main class inherits from WebService and is named NorthwindAnalysisService.

<%@ WebService Language="C#" %> using System.Web.Services; public class NorthwindAnalysisService : WebService { }

You add an @WebService directive to the top of the file to indicate the class implementing the Web service and the programming language used in the implementation. The Class attribute can be set to a class residing in the same file or to a class within a separate assembly (more on this later in the "Precompiled Web Services" section). The Class attribute is important because it indicates which class is to be treated as the Web service class when there is more than one public class defined in the .asmx file.

The Language attribute can be set to a valid ASP.NET language, which as of ASP.NET 1.1 can be C#, Visual Basic .NET, JScript .NET, or J#. (See Chapter 1.) A third attribute can be specified—Debug—which works like Web-page enabling debuggers to step through the source code of the Web service.

The WebService Attribute

The Web service class is normally marked with the WebService attribute. The attribute is optional but should be considered a must for any serious Web service. It allows you to change three default settings for the Web service: the namespace, name, and description.

Within the body of the WebService attribute, you simply insert a comma-separated list of names and values. The keyword Description contains the description of the Web service, whereas Name points to the official name of the Web service.

[WebService( Name="Northwind Analysis Service", Description="Returns customers and orders information")] public class NorthwindAnalysisService : WebService { }

Changing the name and description of the Web service is mostly a matter of consistency. If you don't provide a more descriptive name, the name of the implementing class is assumed as the name of the Web service; no default description is provided.

The Name attribute is used to name the service in the WSDL document that explains its behavior to prospective clients. The description is not used in the WSDL document but is only retrieved and displayed by the IIS default page for URLs with an .asmx extension.

Changing the Default Namespace

Each Web service should have a unique namespace that makes it clearly distinguishable from others. By default, the .NET Framework gives each new Web service the same default namespace: http://tempuri.org. The default namespace should be replaced as soon as possible and certainly prior to publishing the service on the Web.

Note that using a temporary name doesn't affect overall functionality, but it will affect consistency and violate a Web service common naming convention. The only way you have to change the default namespace of an ASP.NET Web service is by setting the Namespace property of the WebService attribute, as shown in the following example:

[WebService( Namespace="ProAspNet/0-7356-1903-4", Name="Northwind Analysis Service", Description="Returns customers and orders information")] public class NorthwindAnalysisService : WebService { }

As discussed earlier, the namespace information is used extensively in the WSDL document that describes the Web service. In the preceding example, I've used the ISBN of this book.

Precompiled Web Services

If you have a precompiled class that you want to expose as a Web service, or if you want to write a Web service using a code-behind class, you can create an .asmx file that contains only the following line:

<%@ WebService %>

The value of the Class attribute denotes the class that implements the Web service. The class must be contained in the Bin subdirectory of the ASP.NET application. The advantage of using precompiled assemblies is that you don't need to deploy a file that contains the source code of the Web service.

Exposing Web Methods

In an ASP.NET Web service, a public method is not accessible to external clients over the network by default. To be accessible over the Internet, a method must be declared public and marked with the [WebMethod] attribute. In practice, the [WebMethod] attribute represents another member modifier like public, protected, or internal. Clients that access the Web service class locally see all the public members; clients accessing the class through the Web service infrastructure see only the methods marked as Web methods. Any attempt to invoke untagged methods via a URL results in a failure.

Note that if the requested method—the SOAPAction attribute of the payload generated by .NET Web services—is not available or not accessible, the HTTP return code is HTTP 500 Internal Error.

The [WebMethod] attribute features a variety of attributes that can be helpful to optimize the interaction with the method. We'll examine the attributes of a Web method later in the chapter in the "Attributes of Web Methods" section.

Defining a Web Method

Let's add a Web method to the NorthwindAnalysisService class. The method is named GetCustomersOrders and has the following prototype:

[WebMethod] public DataSet GetCustomersOrders(string custID, int year)

The input parameters are primitive types—string and integer—but the return type of the method is a .NET Framework type—a DataSet. As we'll see in the next section, return types of Web service methods can always be complex types as long as they can be serialized to XML. The input types are subject to the protocol used to invoke the Web service. If the call goes through the SOAP protocol, complex types such as the DataSet, or custom classes, can be seamlessly used. If the HTTP-POST or the HTTP-GET protocols are used, input types are limited to primitive types.

The GetCustomersOrders method executes a query against the SQL Server Northwind database and returns all the orders that the specified customer issued in the given year. The SQL query is as follows:

SELECT o.customerid, od.orderid, SUM(quantity*unitprice) AS price, o.orderdate, o.shipcountry FROM Orders o, [Order Details] od WHERE o.orderid=od.orderid AND o.customerid=@theCust AND YEAR(orderdate)=@theYear GROUP BY o.customerid, od.orderid, o.orderdate, o.shipcountry

The method uses a data adapter to execute the query. The two parameters of the query—the customer ID and the year—are set through parameters. The source code of the method is shown here:

[WebMethod] public DataSet GetCustomersOrders(string custID, int year) { // Check if a valid custID has been provided if (!IsValidCustomerID(custID)) return null; // Prepare the query string _cmd = String.Format(m_cmdCustOrders, custID, year); SqlDataAdapter _adapter = new SqlDataAdapter(_cmd, m_connectionString); // Add parameters SqlParameter p1 = new SqlParameter("@theCust", SqlDbType.NChar, 5); p1.Value = custID; _adapter.SelectCommand.Parameters.Add(p1); SqlParameter p2 = new SqlParameter("@theYear", SqlDbType.Int); p2.Value = year; _adapter.SelectCommand.Parameters.Add(p2); // Execute the query DataSet _data = new DataSet("NorthwindAnalysis"); _adapter.Fill(_data, "Orders"); return _data; }

Any method in any class, but especially a method in a Web service, should apply the golden rule of secure coding—don't trust user input. In this case, we can be sure about the year argument because the Web service infrastructure will try to convert it to an integer, failing if it contains strings. Can we say the same about the customer ID argument?

To stay on the safe side, we implement a quick validation test. The first time the method is accessed, the whole list of customers is loaded into Application. The passed customer ID is matched against the memory resident list. If no match is found, a null value is returned.

protected virtual bool IsValidCustomerID(string custID) { // Get the list of customers if not available if (Application["CustomerList"] == null) { // Get the list of customers SqlDataAdapter _adapter; _adapter = new SqlDataAdapter("SELECT * FROM customers", m_connectionString); DataSet _customers = new DataSet(); // Creates an in-memory index on the same field // indexed on the database table. An in-memory index // will allow the use of the (fast) Find method to retrieve rows _adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey; _adapter.Fill(_customers, "Customers"); Application["CustomerList"] = _customers; } // Try to find a match for the specified customer ID DataSet _tmp = (DataSet) Application["CustomerList"]; DataTable _table = _tmp.Tables["Customers"]; DataRow _row = _table.Rows.Find(custID); return (_row != null); }

The DataSet type works well with Web services because of its inherently disconnected nature. DataSet objects can store complex information and relationships in an intelligent structure. By exposing DataSet objects through a Web service, you can limit the database connections your data server is experiencing. You should note, though, that the DataSet type cannot be used with input parameters if the request is not made through SOAP packets.

To deploy the Web service, you create a new virtual directory and copy the .asmx file in it. A quick but effective way to test a Web service consists in pointing the browser to the .asmx URL. In this case, IIS provides a default page to let you test the service. The page is shown in Figures 17-6.

Figure 17-6: The test page that IIS displays when you request a Web service from the local machine.

Once you select the method to test, the form in Figure 17-7 pops up.

Figure 17-7: You enter parameters and invoke the method. The page works only through the HTTP-GET protocol. If the method can't be called through HTTP-GET, no form will be displayed.

The XML text returned by the Web method is shown using a new browser window, as in Figure 17-8.

Figure 17-8: The return value for the previous call.

Type Marshaling

Various data types can be passed to and returned from Web service methods. In the .NET Framework, the Web services type marshaling extensively uses the XML serializer—the XmlSerializer class defined in the System.Xml.Serialization namespace. With some limitations that we'll discuss in a moment, all the following types can be used with Web services:

The class working behind type marshaling in the Web service architecture is XmlSerializer. Unlike the runtime serialization formatters, the class doesn't ensure true type fidelity, but simply an effective XSD (or SOAP-encoded) representation of the data being passed back and forth. Thanks to the combined effect of the XML serializer and the proxy classes, the marshaling of data types is transparent to the client application that consumes a Web service.

For example, a .NET client that invokes our Web service gets back a string that is the XML representation of the DataSet class. The string includes the XSD schema of the class. However, the XML representation of the DataSet is a schema that the XML serializer knows because it's part of the .NET Framework. Subsequently, the proxy class that client applications need to effectively call into Web services takes care of the deserialization step and provides the application with a ready-to-use instance of a DataSet object.

If a Linux client attempts to access this Web service, it still gets the same XML representation of the DataSet. In this case, the DataSet schema is likely to be just an unknown schema to the Linux application. As a result, the client application is responsible for parsing its contents and builds any useful representation of the data stored. Depending on the Web service support that the target platform provides, chances are that the DataSet can be automatically mapped to a dynamically generated class that the application can handle. (We'll see more about DataSet and Web services later.)

  Note

For example, on a Java platform proxy classes can be statically generated by the wsdl2Java tool, which works in much the same way as the wsdl.exe utility does for .NET Web services. Interestingly, for an Axis Web service there is also a dynamic call option, similar to .NET Remoting proxies, which makes use of a reflection-like mechanism to detect methods and validate calls.

A .NET client is a bit more fortunate because the proxy class imports the XML schema returned by the method and transforms it into a brand new .NET Framework class.

Note that all the aforementioned types are supported as return values of Web methods whether the Web service is invoked using SOAP, HTTP-GET, or HTTP-POST. As for parameters, the types we just discussed can be used if the protocol is SOAP but not if the method is invoked using HTTP-GET or HTTP-POST. In these cases, in fact, the supported types are only primitives, enumeration types, and arrays of primitives.

Under the Hood of a Web Method Call

Any call made to a Web service method is resolved by an HTTP handler module tailor-made for Web services. Once the incoming call has been recognized as a Web service call and dispatched to the ASP.NET runtime, an instance of the WebServiceHandlerFactory class is created. The factory object compiles the Web service class into an assembly (only the first time) and parses the SOAP headers and contents. If the parse step is successful, the request is transformed into method information. An ad hoc data structure is created to contain information such as the name of the method, the list of formal and actual parameters, whether the method is void, and the returned type.

The method information is then passed to a call handler that will actually take care of executing the method. According to the information specified in the request, the call handler can contain context information (for example, Session) and work either synchronously or asynchronously. Finally, the server object is instantiated, the method is invoked, and the return value gets written to the output stream. Figure 17-9 illustrates the various steps of the process.

Figure 17-9: The steps needed to process a Web service call.

Referencing Web Services from ASP NET

You can invoke a Web service in either of two ways. One possibility requires you to connect to the URL and get back the XML document with the embedded results. The second possibility involves proxy classes. A proxy class is an intermediate class that represents a local intermediary object that mirrors the programming interface of the remote Web service. The proxy class is added to the client project and bridges the local application with the remote server. The proxy can employ a variety of protocols to invoke the Web service, including SOAP and HTTP-POST. The proxy class is most often generated by a system utility and contains an infrastructure to call Web methods both synchronously and asynchronously, and to handle the parameter serialization and return value deserialization.

Building a Proxy Class

If you develop your ASP.NET application using Visual Studio .NET, you'll see that adding a reference to a Web service is not any more complex than adding a reference to a local assembly. You right-click the project node and choose to add a new Web reference. Next, you are shown a search dialog box that allows you to browse for Web services on the local machine, as well as in local and remote UDDI directories. If you know the URL of a particular Web service, you can enter it directly in the dialog box address bar and click to continue. If the Web service is found, a screen that looks like Figure 17-10 will appear.

Figure 17-10: Browsing for Web services and adding references to Visual Studio .NET projects.

When you add a reference to a Web service, Visual Studio .NET generates a proxy class and automatically adds it to the project. The class is created using the language of the project and is given a default name (reference.cs if the project is a C# project). The proxy class defaults to the SOAP protocol to invoke the Web service and hard-codes the URL of the Web service.

  Note

Although Visual Studio .NET by default hides the proxy reference file from view—and in spite of the warnings contained in the source file itself—the file can be modified without any particular concerns. Of course, take this last statement with a grain of salt. It actually means that the file can be modified without worries if you know what you're doing. Safe actions you can take on the reference file are changing the URL, adding some code to methods, adding new methods, and changing the namespace or the base class for the protocol. Altering the structure of the file incorrectly might result in a loss of functionality. In that case, a new reference file must be generated by removing and re-adding the Web reference.

If you're not using Visual Studio .NET, or if you want to keep the proxy class under strict control, you can generate one using the wsdl.exe utility. The utility is located in the C:Program FilesMicrosoft Visual Studio .NET 2003SDKv1.1Bin directory. This application is the same tool that Visual Studio .NET utilizes to generate the proxy class. The command line of the utility is as follows:

wsdl.exe [options] url

Table 17-2 details the various switches you can use.

Table 17-2: Switches Supported by the wsdl.exe Utility

Option

Description

/language:

The language to use for the generated proxy class. Choose from CS, VB, JS, and VJS (J#). The default is CS. Short form is /l:xxx.

/namespace:

The namespace for the generated proxy class. The default namespace is the global namespace. Short form is /n:xxx.

/out:

The filename for the generated proxy class. The default name is derived from the service name. Short form is /o:xxx.

/protocol:

The protocol to implement. Choose from SOAP (default), HttpGet, and HttpPost. You can also choose a custom protocol as specified in the web.config file of the Web service. SOAP 1.2 is expected to be supported in a future release of the .NET Framework.

/username:

/password:

/domain:

The credentials to use when connecting to a server that requires authentication. Short forms are /u:xxx, /p:xxx and /d:xxx.

/proxy:

The URL of the proxy server to use for HTTP requests. The default is to use the system proxy setting.

/proxyusername:

/proxypassword:

/proxydomain:

The credentials to use when connecting to a proxy server that requires authentication. Short forms are /pu:xxx, /pp:xxx and /pd:xxx.

/appsettingurlkey:

The name of the web.config key from which the proxy class reads the URL of the Web service. By default, the URL is hard-coded. Short form is /urlkey:xxx.

/appsettingbaseurl:

The base URL to use when calculating the URL of the Web service. This option must be used in conjunction with the appsettingurlkey option. Short form is /baseurl:xxx.

Called the default way, the WSDL utility generates the following proxy class for the Northwind Analysis Web Service. The command line is:

using System.Diagnostics; using System.Xml.Serialization; using System; using System.Web.Services.Protocols; using System.Web.Services; [System.Diagnostics.DebuggerStepThroughAttribute()] [System.ComponentModel.DesignerCategoryAttribute("code")] [System.Web.Services.WebServiceBindingAttribute( Name="Northwind Analysis ServiceSoap", Namespace="ProAspNet/0-7356-1903-4")] public class NorthwindAnalysisService : SoapHttpClientProtocol { public NorthwindAnalysisService() { Url = "http://localhost/.../northserv.asmx"; } [System.Web.Services.Protocols.SoapDocumentMethodAttribute( "ProAspNet/0-7356-1903-4/GetCustomersOrders", RequestNamespace="ProAspNet/0-7356-1903-4", ResponseNamespace="ProAspNet/0-7356-1903-4", Use=System.Web.Services.Description.SoapBindingUse.Literal)] public DataSet GetCustomersOrders(string custID, int year) { object[] results = Invoke("GetCustomersOrders", new object[] {custID, year}); return (DataSet) (results[0]); } public IAsyncResult BeginGetCustomersOrders(string custID, int year, AsyncCallback callback, object asyncState) { return this.BeginInvoke("GetCustomersOrders", new object[] {custID, year}, callback, asyncState); } public DataSet EndGetCustomersOrders(IAsyncResult asyncResult) { object[] results = EndInvoke(asyncResult); return (DataSet) (results[0]); } }

To communicate with a Web service, you must create a proxy class deriving indirectly or directly from the base class HttpWebClientProtocol. Typical classes you base your proxy on are SoapHttpClientProtocol if you want to go with SOAP packets or HttpPostClientProtocol if you want to go with HTTP-POST commands.

  Note

The base class for a proxy class is different if you're using Web Services Enhancements (WSE) for Microsoft .NET. In this case, the base client protocol class is WebServicesClientProtocol.

When a proxy class is generated for the SOAP protocol, synchronous calls to Web service methods are made via the Invoke method, whereas asynchronous calls are made via BeginInvoke and EndInvoke. The proxy class supports asynchronous calls only if SOAP is used as the protocol. (HTTP-GET and HTTP-POST aren't supported, for example.) The Invoke method prepares the SOAP packets necessary to handshake with the Web service. It takes the name of the method and an array of objects in which elements represent individual method parameters.

Setting the URL of a Web Service

The URL of a Web service is just a public read/write property exposed by the protocol class—the Url property. In situations in which the URL cannot be determined univocally, or might change on a per-user basis or because of other runtime factors, you can ask the wsdl.exe utility not to hard-code the URL in the source. By using the /urlkey command-line switch, you instruct the utility to dynamically read the Web service URL from the application's configuration file. If you use a switch such as / urlkey:ActualUrl, the proxy class constructor changes as follows:

public NorthwindAnalysisService () { string urlSetting = ConfigurationSettings.AppSettings["ActualUrl"]; if ((urlSetting != null)) Url = urlSetting; else // defaults to the URL used to build the proxy Url = "http://localhost/.../northserv.asmx"; }

The URL is read from the web.config file by using the AppSettings collection.

Disabling HTTP-POST and HTTP-GET

While HTTP-POST and HTTP-GET make accessing a Web service easier than ever, they leave the Web service door open to any HTTP packets. The SOAP protocol for the filtering capabilities of the SOAPAction attribute—especially when considering the syntax of the payloads—is inherently more resistant to malicious attacks than HTTP-POST and HTTP-GET. For this reason, you might want to configure your Web service so that its results are accessible only through the SOAP protocol.

If you want to disable HTTP-POST and HTTP-GET support, make sure the local web.config file contains the following text:

The HTTP-POST and HTTP-GET protocols will remain enabled for all Web services except the one that explicitly disables them. To disable them for all Web services on the machine, you must update the machine.config file instead. Under the same path, you should simply comment out the nodes that relate to POST and GET.

  Note

If you look at the section in web.config, you can't help but notice the special Documentation protocol. The protocol is the key that enables the ASP.NET runtime to deliver a test page when you point your browser to a .asmx resource. The default page is generated by a file named DefaultWsdlHelpGenerator.aspx located in the same folder as machine.config. The page can be replaced through the following configuration code:

 

Of course, the help page can be customized for all Web services or only for a particular Web service. In the latter case, you should enter the previous changes in the local web.config file.

Note that ASP.NET 1.1 enables an extra protocol named HttpPostLocal. This protocol automatically enables posting to the Web service through the .asmx URL only from the local machine. If you want the ASP.NET engine to show the test user interface of .NET Web services to remote users, you must disable the HttpPostLocal protocol in the web.config file.

Invoking Methods on a Web Service

Let's see how to build a sample ASP.NET page to consume the Web service. Once you created the proxy class, you should make it available to the ASP.NET application. If the application is being developed through a Visual Studio .NET project, you simply add the file to the project or, better yet, import the Web service automatically by using the Add Web Reference menu. If you're not using Visual Studio, you must compile the proxy class into an assembly and make the assembly available in the Bin subdirectory of the Web application.

For example, you can manually compile a C# proxy class by using the following command:

csc /out:northservproxy.dll /t:library /r:System.Xml.dll, System.Web.Services.dll NorthwindAnalysisService.cs

The resultant assembly is named northservproxy.dll and references internally the System.Xml and System.Web.Services assemblies. Note that csc.exe is the C# compiler provided with the .NET Framework.

If the assembly with the proxy class is available in the Web space of the application, the ASP.NET page can use the proxy class as if it were an ordinary class of the .NET Framework. The following source code depicts a Web page with a couple of drop-down lists to pick up customer and year and a link button to retrieve orders:

<%@ Page Language="C#" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %>


The output of the page is shown in Figure 17-11.

Figure 17-11: A sample page using a Web service.

  Caution

In the preceding code, we used the SelectedValue property of the DropDownList Web control. You should note that the property is supported only in ASP.NET 1.1. The SelectedValue is exposed by the ListControl class from which CheckBoxList, RadioButtonList, and ListBox also inherit. The property is read/write and, as such, can be used to get and set the currently selected value (not the text). To make the preceding code work with ASP.NET 1.0, replace SelectedValue with SelectedItem.Value.

Web Methods Best Practices

While exposing a Web method is an easy task to accomplish, blending method attributes well to set up an effective call is not always easy. The execution of a Web method is a function of several parameters and can be optimized in many ways. Regardless of the magic performed by the proxy class, which gives the illusion of a local call, a Web method is a very special breed of code. It executes remotely, and it carries parameters and return values back and forth on top of a transportation protocol. This characteristic raises a few issues.

A Web method can certainly throw exceptions, but how do you handle them on the client? A Web method can execute in the context of a transaction, but can you realistically think of enlisting a Web service in a distributed transaction? A Web method call goes through the Internet and is likely to take seconds to complete. How can you minimize the impact of this structural aspect, though? Let's examine a few best practices for designing and implementing Web methods.

Attributes of Web Methods

The [WebMethod] attribute supports some properties that can be used to tune the behavior of the method. Table 17-3 lists the properties.

Table 17-3: Properties of the WebMethod Attribute

Property

Description

BufferResponse

Set to true by default, this property indicates that IIS should buffer the method's entire response before sending it to the client. Even if set to false, the response is partially buffered; however, in this case, the size of the buffer is limited to 16 KB.

CacheDuration

Specifies the number of seconds that IIS should cache the response of the method. This information is useful when you can foresee that the method will handle several calls in a short period of time. Set to 0 by default (meaning no caching), the caching engine is smart enough to recognize and cache page invocations that use different parameter values.

Description

Provides the description for the method. The value of the property is then embedded into the WSDL description of the service.

EnableSession

Set to false by default, this property makes available to the method the Session object of the ASP.NET environment. Depending on how Session is configured, using this property might require cookie support on the client or a SQL Server 2000 installation on the server. (See Chapter 14.)

MessageName

Allows you to provide a publicly callable name for the method. When you set this property, the resulting SOAP messages for the method target the name you set instead of the actual name. Use this property to give distinct names to overloaded methods.

TransactionOption

Specifies the level of COM+ (local) transactional support you want for the method. A Web service method can have only two behaviors, regardless of the value assigned to the standard TransactionOption enumeration you select: either it does not require a transaction or it must be the root of a new transaction.

The following code snippet shows how to set a few method attributes:

[WebMethod( CacheDuration=60, Description="Return orders for a customer in a given year")] public DataSet GetCustomersOrders(string custID, int year) { }

Attributes must be strongly typed in the declaration. In other words, the value you assign to CacheDuration must be a number and not a quoted string containing a number.

Caching the Output of a Web Method

The CacheDuration property is implemented using the ASP.NET Cache object, which proves once again the tight integration between Web services and the ASP.NET runtime. Just before instantiating the Web service class, the Web service handler configures the Cache object. In particular, the handler sets the cache to work on the server, as shown here:

Response.Cache.SetCacheability(HttpCacheability.Server);

In addition, the handler sets the expiration time and configures the caching subsystem for parametric output, as follows:

Response.Cache.VaryByHeaders["SOAPAction"] = true; Response.Cache.VaryByParams["*"] = true;

As we saw in Chapter 14, the VaryByHeaders property enables you to cache multiple versions of a page, depending on the value of the HTTP headers you specify (in this example, the header value is SOAPAction)—that is, the method invoked. The VaryByParams property, on the other hand, lets you maintain different caches for each set of distinct values of the specified parameter. In this case, using the asterisk (*) indicates that all parameters must be considered and cached separately.

As shown in an earlier CacheDuration code snippet, IIS maintains the output of calls for each distinct pair of customer ID and year. The cache is global and accessible to different sessions. An easy way to verify that the output of the method is being cached is storing somewhere in the DataSet the time at which the returned data is collected. The DataSet ExtendedProperties collection seems to be the ideal place for this kind of information.

// Fill the DataSet and store the time the query occurred DataSet _data = new DataSet("OrderList"); _data.ExtendedProperties["QueryTime"] = DateTime.Now.ToString("hh-mm-ss"); _adapter.Fill(_data, "Orders");

Unfortunately, there's no automatic and generic way for a client page to detect whether the output it gets is cached or not. Figure 17-12 shows output being cached, but we're simply assuming the page knows about the information stored in the ExtendedProperties collection. Also, because the output cache is managed outside the Web service class, the client has no way to specify that it requires fresh data.

Figure 17-12: The client page demonstrates that the Web service returns cached output.

Under certain conditions, the CacheDuration attribute can constitute a significant improvement for your Web services. Ideally, you might want to set this attribute when your method queries for data and adds a database overhead. Using output caching amortizes the cost of retrieving data over more user calls, thus increasing the efficiency.

Minimizing the Data Transferred

When you have to return data objects, the DataSet seems to be the most flexible data container you can ever dream of. However, consider that the DataSet serializes in the DiffGram format plus the XSD schema. (See Chapter 5.) Although rich and flexible, a DiffGram is also quite verbose. The same information packed in a DataSet could be returned in a much more compact format if only you drop the DataSet class in favor of an array of custom classes. Let's see how.

The OrderInfo class is defined in the Web service as a public class. It maps through public properties the columns of the query run against the database. In other words, it looks like an extremely lightweight DataRow object.

// Helper class used to minimize the amount of data returned to the client public class OrderInfo { public string CustomerID; public int OrderID; public DateTime OrderDate; public float Total; public string ShipCountry; }

The new method GetCustomersOrdersInfo first calls the GetCustomersOrders method, gets a DataSet, and then packs all the contained info into an array of OrderInfo classes. (Of course, you could also optimize the code that executes the query.)

[WebMethod] public OrderInfo[] GetCustomersOrdersInfo(string custID, int year) { // Get the data to return DataSet _data = GetCustomersOrders(custID, year); if (_data == null) return null; DataRowCollection _rows = _data.Tables[0].Rows; // Allocate the array OrderInfo[] info = new OrderInfo[_rows.Count]; // Fill the array (assume no DBNull values are found) int i=0; foreach(DataRow _row in _rows) { OrderInfo o = new OrderInfo(); o.CustomerID = _row["CustomerID"].ToString(); o.OrderID = Convert.ToInt32(_row["OrderID"]); o.OrderDate = DateTime.Parse(_row["OrderDate"].ToString()); o.Total = Convert.ToSingle(_row["Total"]); o.ShipCountry = _row["ShipCountry"].ToString(); info[i++] = o; } return info; }

The client application doesn't know anything about the OrderInfo class. However, the proxy class reads its definition out of the WSDL document and creates an equivalent in the specified language. As a result, the ASP.NET client page can happen to work with a structure it was not designed to handle! You should note, though, that it's far more common to establish Web services between the client and middle-tier components in a single distributed application, or between federated applications. In these cases, the communicating sides share object definitions (assemblies if the .NET Framework is used) and are built with knowledge of the data they receive.

Figure 17-13 shows the client page for the NorthwindAnalysisService Web service. The first table uses the GetCustomersOrders method, which returns a DataSet. The second table uses the GetCustomersOrdersInfo method, which returns an array of OrderInfo classes.

Figure 17-13: The client page extended to support the new GetCustomersOrdersInfo Web method. The second table has been created using a Repeater bound to the array of OrderInfo classes.

  Note

It's rather obvious that when a class defined within the Web service file is returned (for example, a Java class if the Web service is hosted on Apache and run by Axis), the proxy class can only build a client-side representation of it. In other words, the serializer simply reproduces the public programming interface of the class limited to public properties. No code the class might originally contain is replicated.

You should use the DataSet only if you return multiple tables with relations, indexes, extended properties, and the like. As long as you use the DataSet only to contain a single read-only table, an array of custom structures is more effective in terms of bandwidth usage.

The Debate Around Web Services and DataSets

In many respects, working with the DataSet class is like having a simple, in-memory version of a relational database. You have tables and relations, indexes and views, and constraints. Could you ask for more? Furthermore, the class works disconnected from any data source you might have used to fill it and is totally serializable. When serialized, the DataSet returns a DiffGram XML schema that also allows you to maintain the story of the changes entered to the data. What's wrong with the DataSet class in the context of Web services?

Well, there are two things, one of which we already mentioned a moment ago—the size of the data is rather bloated. The second aspect is complexity. Imagine what happens when the DataSet is returned to clients running on other platforms. For sure, those clients have all that is needed to make sense of the data, but this alone doesn't mean that those clients are going to have an easy time parsing and extracting data. What seems to be the most savvy approach is overloading those methods that have to return database-like data so that they return the same data in three equivalent formats—a DataSet, an array of custom types, or even raw XML. In any case, an output alternative to DataSet objects can easily be built from the DataSet itself.

Throwing Exceptions

Any exception a Web service method might throw on the server is received on the client as an exception of a fixed type. If the client accessed the method over SOAP, a SoapException exception is raised on the client. If HTTP-POST or HTTP-GET was used, the type of the exception is WebException. Both exceptions wrap the original exception that was caught on the server, thus providing a uniform programming interface. The original exception object, though, won't be available on the client. To get more information, you can only rely on the properties exposed by the two exception classes.

If the client is known to issue calls through SOAP, there's a better way to throw exceptions. In this case, the Web method throws a SoapException and fills some of its properties. Some of the properties of the SoapException class are listed in Table 17-4.

Table 17-4: Properties of the SoapException Class

Property

Value

Actor

Indicates the actor that caused the SOAP fault. Typically, in the current implementation of the .NET Framework, the actor is the URL of the Web service method. In the SOAP specification, the actor is an intermediary in the message processing chain. The message jumps from one actor to the next until the final destination—another actor—is reached. Along the way, one actor could find an error and throw an exception. That object would be referenced by this property. As of today, the .NET Framework doesn't fully support actors yet.

Code

Indicates the type of the SOAP fault. When set, ServerFaultCode denotes an error occurred during the processing of a client call on the server—not an error due to a badly formed SOAP message. Other fault codes are ClientFaultCode, VersionMismatchFaultCode, and MustUnderstandFaultCode.

Detail

Gets an XmlNode representing the application-specific error information.

Message

The Message property of the original exception.

The following code illustrates the recommended way of throwing an exception from a Web service method:

if (_data == null) { SoapException se = new SoapException("Invalid Customer ID", SoapException.ServerFaultCode, Context.Request.Url.AbsoluteUri); throw se; }

Figure 17-14 shows how an exception appears on the client.

Figure 17-14: An unhandled SOAP exception raised by a Web method call.

In the code life cycle, exceptions should just be exceptional events. Working with exceptions is particularly easy, and the resulting code is significantly more readable. So what's wrong with exceptions? Generally speaking, exceptions are quite expensive in terms of CPU involvement. More importantly, whenever an exception is inserted in the code, the CLR does some extra work simply because an exception construct is met. When an exception is finally caught, that's the peak of pressure on the CPU. I could agree that this extra burden is less relevant in the context of a Web service call, which is a "slow" call by design. However, as a general guideline, you should throw exceptions just in exceptional circumstances when no other static way exists to trap the error.

  Note

This advice about being careful with exceptions is simply a warning about the possible performance hit that result from the abuse of exceptions. In no way should it sound like an encouragement to trap errors through cryptic HRESULT codes.

Transactional Methods

The behavior of a Web service method in the COM+ environment deserves a bit of attention. The inherent reliance of Web services on HTTP inevitably prevents them from being enlisted in running transactions. In the case of a rollback, in fact, it would be difficult to track and cancel performed operations because HTTP is stateless, the network is unreliable, and latency introduces too much nondeterministic behavior. For this reason, a Web method can do one of two things: it can work in nontransacted mode, or it can start a nondistributed transaction.

For consistency, the TransactionOption property of the WebMethod attribute takes values from the .NET Framework TransactionOption enumeration. Some of the values in this enumeration have a different behavior than is expected or documented, however. In particular, the Disabled, NotSupported, and Supported values from the TransactionOption enumeration always cause the method to execute without a transaction. Both Required and RequiresNew, on the other hand, create a new transaction.

You can use the ContextUtil class from the System.EnterpriseServices namespace to explicitly commit or abort a transaction. The class exposes the SetComplete and SetAbort methods. In general, SetComplete indicates that the caller votes to commit its work, whereas SetAbort indicates that the object encountered a problem and wants to abort the ongoing transaction. When these methods are used from within a transacted Web service, the vote has an immediate effect and the transaction is committed or aborted, as required.

  Note

When a transactional method throws an exception or an externally thrown exception is not handled, the transaction automatically aborts. If no exceptions occur, the transaction automatically commits at the end of the method being called.

Asynchronous Methods

Web services support both synchronous and asynchronous invocation. Under synchronous communication, the client sends a request for a method and waits for the response. While waiting for the results, the client can't perform other operations. In an asynchronous scenario, on the other hand, the client can continue with other tasks as it waits for a response on a different thread. The proxy class contains both synchronous versions and asynchronous versions of the methods in a Web service.

The asynchronous versions consist of two methods named BeginXXX and EndXXX, where XXX is the name of the method. The BeginXXX method is used to initiate the Web service, while the EndXXX method retrieves the results. The following code snippet shows how to set up an asynchronous call of a method that adds two integers:

// Calling Add(2,2) int a=2, b=2; IAsyncResult result = service.BeginAdd(a, b, null, null); // Wait for asynchronous call to complete. result.AsyncWaitHandle.WaitOne(); // Complete the asynchronous call to Web service method int total = service.EndAdd(result);

How does the client know when the operation completed? There are two methods used to determine this. The first method entails that a callback function be passed into the BeginXXX method. The Web service infrastructure will then call this function back when the method has completed processing. The second method (shown previously) is that a client waits for the method to complete using the WaitOne method of the WaitHandle class. The AsyncWaitHandle property exposed by the IAsyncResult interface is just a member of the WaitHandle type.

  Note

Regardless of whether the method is invoked synchronously or asynchronously, the SOAP messages sent and received are identical in the two cases.

Securing Web Services

In Chapter 15, we already covered several aspects of ASP.NET security. To briefly summarize, any request that hits IIS is first processed by the Web server. If the request is legitimate, the control passes to the ASP.NET application. The ASP.NET application can decide whether to rely on the IIS response and allow the user to operate or implement a more restrictive policy.

If the ASP.NET application is configured for Windows authentication, the security token received by IIS is considered valid. Next, ASP.NET looks at whether the authenticated user is authorized to access the requested resource. Users can be allowed or denied access to a resource by using the section in the web.config file. The following code snippet details users that can access a particular resource:

 

Web services are just a special flavor of ASP.NET applications; subsequently, the implementation of a security layer exploits many of the concepts we examined in Chapter 15. Access to Web services can be controlled in two ways—through a Windows user account and through a custom authentication scheme that can be considered as the Web service counterpart of the ASP.NET Forms authentication. Let's consider Windows authentication first.

  Important

The built-in security mechanism provided by the .NET Framework for Web services is sometimes limiting in the real-world. For a much more robust security framework, have a look at the Web Services Enhancements for Microsoft .NET.

HTTP User Authentication

To require Windows authentication on a given Web service, first ensure that anonymous access is disabled. Next, you enable Integrated Windows authentication for the .asmx resource. Figure 17-15 shows the properties and Authentication Methods dialog boxes for the Web service obtained within the IIS Manager.

Figure 17-15: The first thing to do to secure a Web service is disable anonymous access and enable Integrated Windows authentication.

To allow or deny specific users access to the Web service, use the ASP.NET configuration system or set access control lists (ACLs) on the .asmx file itself. The web.config snippet shown previously is a good starting point.

Making a Secure Call

So much for the configuration of the environment, let's see in which way a client page can invoke a secured Web service. There are two possibilities. If the page invokes a method as usual—that is, without taking any measures—the call is successful as long as the client Windows account of the user is authenticated on the Web server and authorized by the Web service web.config file. As you can see, in this case everything happens in a codeless way.

Another scenario is when there's no correspondence between the Windows account of the calling machine and the credentials submitted to the Web service. In this case, the caller page operates as another user. The NetworkCredential class in the System.Net namespace acts as a credentials container. It can be used to provide credentials for password-based authentication schemes such as Basic, Digest, NTLM, and Kerberos authentication. The code of the client page is as follows:

<%@ Page Language="C#" %> <%@ Import Namespace="System.Net" %>

Pro ASP.NET (Ch17)

 

 

A good question is, where does the Credentials property come from? The property is exposed by the WebClientProtocol class from which any Web service proxy class inherits. The credentials stored in the proxy class are silently used when it is time to connect to the Web service.

Detecting the Connected User

The Web service can grab information about the connected user thanks to the User property of the HTTP context. The following code shows a simple Web service that just says hello to each authorized connected user:

<%@ WebService Language="C#" %> using System.Web.Services; using System; class SecureService : WebService { [WebMethod] public string GuessWhoIs() { return "Welcome " + User.Identity.Name; } }

Figure 17-16 shows this code in action.

Figure 17-16: A sample Windows-based secure Web service.

In general, Windows authentication is mostly appropriate for intranet applications that authenticate users in the current domain. On the Internet, though, you need to use a SQL database to store user names and passwords. Forms authentication that works great for conventional ASP.NET applications is not a model that works for Web services because of its inherent interactivity. An alternative scheme for custom authentication and authorization can be performed making use of SOAP headers.

Custom User Authentication

To authenticate the user that is going to execute a Web method, credentials must be passed along with the request. The format you use for the credentials is custom and service-specific. In general, valid credentials are any sequence of values that the Web service can validate and successfully transform in a user identity. At a minimum, valid credentials include a user ID and a password.

As mentioned earlier in "The SOAP Payload" section, a SOAP message can optionally contain headers. This means that a SOAP header is ideal for transmitting free information not strictly related to the semantics of a Web service. A Web service that intends to use the custom SOAP authentication must do a couple of things. First, it must indicate that the SOAP header is expected to contain the authentication credentials. Second, it must process the credentials and determine whether or not the request can be authorized.

Embedding Credentials in the SOAP Payload

Let's create a new class to hold the credentials of the connecting user. In the Web service source code, we derive a new class from SoapHeader.

// UserInfoHeader represents the custom SOAP header public class UserInfoHeader : SoapHeader { public string UserName; public string Password; }

The class represents the custom SOAP header we're going to use to hold the credentials for any secure method we define. The second step consists of declaring a publicly accessible member of the UserInfoHeader type.

// Must use a namespace different from that of the proxy class namespace ProAspNet.CS.Ch17 { public class SecureNorthwindAnalysisService : WebService { // Custom authentication header public UserInfoHeader UserInfoHeaderValue; [WebMethod] [SoapHeader("UserInfoHeaderValue", Required=true)] public DataSet GetCustomersOrders(string custID, int year) { } } }

The UserInfoHeaderValue property is exposed in the WSDL document for the service and made available to the client when the proxy is created. Any method that you want to protect against unauthorized callers features an extra [SoapHeader] attribute. Table 17-5 shows the properties of this attribute.

Table 17-5: Properties of the [SoapHeader] Attribute

Property

Description

Direction

Gets or sets whether the SOAP header is intended for the Web service, the Web service client, or both. By default, the header is for the Web service only.

MemberName

Gets or sets the member of the Web service class representing the SOAP header contents. This is the default property of the attribute.

Required

Gets or sets a value indicating whether the SOAP header must be understood and processed by the recipient Web service or Web service client. True by default.

The MemberName of the attribute is set with the public property of the Web service class that will be filled with the contents of the header. Because this is the default property, you don't need to indicate it by name. In the preceding code snippet, we just set userInfo as the member associated with the header. Note that you can use multiple headers and a different header for each method to secure.

In light of the custom header, the SOAP envelope for the GetCustomersOrders method of our example Web service is slightly different.

string string string int

  Important

The SOAP header is sent over the network in clear text. The fact that a custom header is used doesn't automatically enable any protection mechanism. It's obvious that transmitting passwords as clear text is not a good practice, especially if you're just trying to make the Web service more secure. To encrypt transmitted data, add an encryption algorithm using the .NET Framework cryptographic API. For example, you could pass the password as a SHA1 hashed value and compare it against an identically hashed value retrieved from the database:

byte[] pswd = new byte[PSWD_SIZE]; byte[] hashedPswd; SHA1 sha = new SHA1CryptoServiceProvider(); hashedPswd = sha.ComputeHash(pswd);

Authenticating the User

The proxy class generated for the Web service includes a definition for the UserInfoHeader class. To avoid run-time name conflicts, make sure the definition of the UserInfoHeader class in the proxy class and the Web service belong to different namespaces.

The client page first fills a new instance of the SOAP header class with its credentials. Next, it associates the custom header with the property of type UserInfoHeader on the Web service proxy.

// Instantiate the Web service proxy class SecureNorthwindAnalysisService serv = new SecureNorthwindAnalysisService(); // Instantiate the SOAP header class UserInfoHeader user = new UserInfoHeader(); // Store credentials user.UserName = "joeusers"; user.Password = "loveAspNet"; // Bind the proxy class with the header serv.UserInfoHeaderValue = user;

When a call is made, the invoked Web method will find its public member of type UserInfoHeader set with the credentials. Of course, the credentials are needed only for the methods marked with the [SoapHeader] attribute. The Web method will use the information stored in the credentials class to authenticate the user.

public DataSet GetCustomersOrders(string custID, int year) { // Authenticate the user if (!AuthenticateUser()) { // or throw an exception... return null; }

AuthenticateUser is a local function you can implement as needed. The typical implementation is nearly identical to the authentication function we considered in Chapter 15 within the context of form-based authentication.

protected virtual bool AuthenticateUser() { // ProAspNet database, Users table string connString, cmdText; connString = "SERVER=localhost;DATABASE=proaspnet;UID=sa;"; cmdText = "SELECT COUNT(*) FROM users WHERE id=@User AND pswd=@Pswd"; SqlConnection conn = new SqlConnection(connString); SqlCommand cmd = new SqlCommand(cmdText, conn); SqlParameter p1 = new SqlParameter("@User", SqlDbType.VarChar, 25); p1.Value = UserInfoHeaderValue.UserName; cmd.Parameters.Add(p1); SqlParameter p2 = new SqlParameter("@Pswd", SqlDbType.VarChar, 12); p2.Value = UserInfoHeaderValue.Password; cmd.Parameters.Add(p2); conn.Open(); int found = (int) cmd.ExecuteScalar(); conn.Close(); return (found >0); }

Managing Roles

Once the user has been authenticated, a new principal can be created and bound to the Context.User property. In doing so, you could also retrieve and store the roles associated with the user, if any. The following code snippet shows how to rewrite the AuthenticateUser method so that it checks for any role information in the database and sets the new user identity.

protected virtual bool AuthenticateUser() { string connString, cmdText; connString = "SERVER=localhost;DATABASE=proaspnet;UID=sa;"; cmdText = "SELECT role FROM users WHERE id=@theUser AND pswd=@thePswd"; SqlConnection conn = new SqlConnection(connString); SqlCommand cmd = new SqlCommand(cmdText, conn); SqlParameter p1 = new SqlParameter("@User", SqlDbType.VarChar, 25); p1.Value = UserInfoHeaderValue.UserName; cmd.Parameters.Add(p1); SqlParameter p2 = new SqlParameter("@Pswd", SqlDbType.VarChar, 12); p2.Value = UserInfoHeaderValue.Password; cmd.Parameters.Add(p2); int found = 0; string[] roles = null; conn.Open(); SqlDataReader _reader = cmd.ExecuteReader(); while(_reader.Read()) { found ++; string roleString = _reader["role"].ToString(); roles = roleString.Split(','); } _reader.Close(); conn.Close(); bool isAuth = (found >0); // Set the new user identity if (isAuth) { GenericIdentity ident; ident = new GenericIdentity(UserInfoHeaderValue.UserName); Context.User = new GenericPrincipal(ident, roles); } return isAuth; }

Associating the principal with roles information lets you distinguish the results of each method based on the role of each user.

[WebMethod(CacheDuration=30)] [SoapHeader("UserInfoHeaderValue", Required=true)] public DataSet GetCustomersOrders(string custID, int year) { // Authenticate the user if (!AuthenticateUser()) { // or throw an exception... return null; } // Check the role. If the role is Employee, then // no more than 3 rows should be returned string _cmd; if (User.IsInRole("Employee")) _cmd = String.Format(m_cmdCustOrders, 3); else _cmd = String.Format(m_cmdCustOrders, "100 PERCENT"); // Should check if a valid custID is provided if (!IsValidCustomerID(custID)) return null; // Prepare the query

The method GetCustomersOrders now performs three different checks. First, it ensures that the user has the rights to do what she's trying to do. Second, it adapts the output to the role of the user. Finally, it validates the input parameters.

Using Application Specific Tokens

The Web service infrastructure does not mandate that you implement authentication and authorization by using a made-to-measure API or approach. The implementation of authentication and authorization is left up to the application, which sets up an effective scheme exploiting the capabilities of the application's platform and protocol. The approaches to Web service security we discussed so far are extensively based on characteristics of the Windows platform and SOAP.

In the Windows authentication case, you can configure your security layer in a rather codeless way but at the price of forcing users to have a user account on the Web service machine. The second approach, based on custom SOAP headers, is more flexible but requires that credentials are passed for each method invocation. Also note that by default credentials are passed as clear text unless you manually encrypt them.

A consolidated scheme for authenticating users of a remote service is described next. Instead of passing credentials every time a Web service call is made, users manage to obtain a token (either statically or dynamically) and then pass it along with other parameters when making a call. The advantage is that you can more easily control the validity of the token, implement expiration policies, and provide encryption without encrypting credentials every time.

Getting the Token

You can design your Web service so that it requires all clients to log on prior to calling any methods. The signature of the method for logging on might look like the following:

long Login(string userName, string pswd)

The method returns a value that works as a token to call other methods. The information stored in the token, as well as its data type, is completely up to you. You can use a number, GUID, or string. The token should be several bytes long, unique, random, and overall hard to remember for humans. In addition, you should be able to extract from it all the information needed to verify the authenticity of the token and the user prior to calling the requested method. This can be obtained by either hashing the supplied credentials in a bijective mapping or, more simply, using a key value and database to track map tokens and users.

The following pseudocode shows how it works, assuming that the token is a long:

// Credentials should be encrypted when logging in long token = MyService.Login(uid, pswd); // Add the token to all successive calls MyService.GetEmployee(token, nEmpID);

The Login method must authenticate the user against a database or any persistent support you might want to consider. It then creates and returns a token that summarizes information about the user and the time the token was generated. Each method that requires the token first checks its validity before resuming execution.

The Amazon Case

In the summer of 2002, Amazon (http://www.amazon.com) released the first version of its Web services, which allowed users to programmatically search for prices, products, and availability information as well as to manage shopping carts. The Amazon Web Services Developer's Kit can be downloaded from http://associates.amazon.com/exec/panama/associates/join/developer/kit.html. The developer's kit contains documentation as well as code samples for Java, Perl, SOAP, XML, and XSLT. No .NET samples are provided. The Amazon Web Services Developer's Kit, or the similar kit released by Google, is useful because it provides a concrete, real-world service to experiment with.

You can get the WSDL for the Amazon Web Services from the following URL: http://soap.amazon.com/schemas2/AmazonWebServices.wsdl. The proxy class can be generated with the following script:

wsdl.exe /out:aws.cs amazonwebservices.wsdl

For more details about programming with the Amazon Web Service, take a look at the documentation, but first make sure you have at least a working knowledge of the data structures involved.

However, what's interesting about the Amazon Web Services is the architecture of security. To call into the Amazon Web Services, you must first get your token by registering with the site. You can get your developer's token by visiting the following URL: https://associates.amazon.com/exec/panama/associates/join /developer/application.html.

The token is generated offline and handed to users in a static manner, as opposed to the programmatic login we considered earlier. That token, though, must be added as an argument to each method call. The Amazon token is an alphanumeric string.

Managing the State of Web Services

Web services can access the same state management features as other ASP.NET applications. For this to happen, though, the Web service class must derive from the WebService class, which contains many common ASP.NET objects, including the Session and Application objects. Classes that inherit from WebService don't need to take particular measures to access global state objects such as the Application or Cache object. Working with the session state is a bit trickier.

Enabling Session State

As mentioned, ASP.NET session support for Web methods is turned off by default. To use session state within a Web method, you must first set the EnableSession property of the [WebMethod] attribute to true. Note that support for the session state must be enabled on a per-method basis. If the Web service exposes two methods that need to deal with the session memory, the EnableSession property must be turned on for both.

Note that the requirement of session state is a transitive property. If, say, method A doesn't require session state directly but internally calls into method B that, on the other hand, does require state management, then sessions must be enabled for both methods.

  Note

If session support is disabled at the web.config level, then regardless of the value of EnableSession, the session state is always inaccessible to all pages affected by the web.config, including Web services. For more information on configuration files, see Chapter 12.

The value of the CacheDuration property affects the management of the session state in Web methods. When CacheDuration is set to a value greater than zero, the output of the method is cached for the specified number of seconds. This means that for the given duration any request for the method is served by returning the cached XML text rather than executing the code of the method. As a result, as long as the cached output is valid, the session state is not updated. This behavior affects both ASP.NET pages and Web services.

Using Cookie Containers

Unless you change the ASP.NET default settings, session state is implemented by using cookies and storing the collection of data items within the Cache object. When a Web method is called that uses session state, a cookie is passed back to the client in the response headers. The cookie uniquely identifies the session state as maintained by ASP.NET for that Web service.

The Importance of a Cookie Container

Suppose that a Web method contains code as in the following listing. The method updates an internal counter whenever called. With reference to the role discussion of the previous example, you could use this counter to stop requests from users with less privileges.

// Count accesses if (Session["Counter"] == null) Session["Counter"] = 1; else Session["Counter"] = ((int) Session["Counter"]) + 1;

Let's consider what happens the first time the method is invoked. The ASP.NET client issues the request; the Web method executes, and it sets the counter to 1. The Web response contains the cookie that represents the Web service session state. Unless the browser itself manages to send the request and get the response, the cookies in the response are lost. If you test the Web service using the browser-based sample test page, everything works great from the beginning. However, as long as the effective client of the Web service becomes a proxy class (either a Windows Forms or Web Forms class), a made-to-measure cookie container is needed.

Building a Persistent Cookie Container

It's not coincidental that the proxy class—specifically, the base class HttpWebClientProtocol—exposes a CookieContainer property. For the Web service client to receive the session cookie, a new instance of CookieContainer must be created and assigned to the CookieContainer property before calling the stateful Web service method. This will ensure that the cookie is properly included in all subsequent requests.

In this way, when handling the successive Web method request, the ASP.NET runtime finds in the attached cookie list one that represents the session to bind to the Web service context. As a result, a session-state instance with a matching ID is loaded from the storage medium (cache, server, or SQL Server) and bound to the HTTP context. The Web service, therefore, retrieves the right session state.

Not only must a cookie container be used, but if the Web service client is an ASP.NET page, it has to be persistent too. The methods that follow illustrate how to bind the cookie container to the proxy class and how to save it for future retrieval by client session:

void BindCookieContainer(HttpWebClientProtocol proxy, string sessionSlot) { CookieContainer cc; // Retrieve the cookie container from the page's Session state if (Session[sessionSlot] == null) cc = new CookieContainer(); else cc = (CookieContainer) Session[sessionSlot]; // Bind the cookie container to the proxy proxy.CookieContainer = cc; } void SaveCookieContainer(HttpWebClientProtocol proxy, string sessionSlot) { Session[sessionSlot] = proxy.CookieContainer; }

The cookie container we bind to the proxy class must be persisted across postbacks to provide an effective storage mechanism analogous to that of the browser, which uses an internal folder. In particular, we store the previous instance of the CookieContainer class in a session slot with a unique name. When binding the cookies to the proxy class, you call the method BindCookieContainer; when done with the method, the proxy CookieContainer property contains all the cookies that arrived with the response. They must be cached for further use by the client.

The following code snippet shows how the client should wrap a call to a stateful Web method. Figure 17-17 illustrates how the counter changes after two invocations of stateful methods.

Figure 17-17: An ASP.NET client calling stateful methods on a Web service.

// CookieJar is simply the (fancy) name of the session slot // where the cookie container is persisted BindCookieContainer(serv, "CookieJar"); serv.StatefulMethod(...); SaveCookieContainer(serv, "CookieJar");

Conclusion

The world of Web services is evolving very quickly. Over the past couple of years, specifications have been first defined, then revised, and in some cases, even deprecated. Web service toolkits have shipped, and developers have tried to use them to build systems. In doing so, they discovered a number of issues that, unlike what often happens with ordinary code, reinforced the positive feeling toward the technology and self-propelled its progress.

A lot of work has been done with Web services over the past few years, but even more remains to be done. And should the remaining work be abandoned, the feeling is that the whole Web service technology would starve in a matter of months. What does this mean to you? It means you can't give up at this time. If you're going to build Web service–based systems, you should understand where the platform is today and what the future has in store for us all.

Today, the work and research are geared in two directions. While W3C focuses on new versions of the core Web service specifications, a separate organization focuses more on interoperability. The Web Services Interoperability Organization (WS-I) is committed to defining a series of best practices for making services work seamlessly.

So far the experience of working with Web services proved the need of a standard way to secure Web services, route messages reliably, and define transactional semantics. These are just a few structural issues that apply to all domains of applications and business implemented through Web services. The Global XML Web Service Architecture (GXA) initiative is aimed at defining specifications for these infrastructure-related services—a sort of abstract virtual machine for Web services.

In this chapter, we focused on Web services from the .NET perspective and also limited our look at ASP.NET clients. However, all the key issues and caveats have been discussed. If you would like to learn more about SOAP and Web services in general, many wonderful resources are available on the Web. Here are a few recommended links:

For a developer with .NET-oriented vision, nothing is better than the Microsoft XML Web service home at http://msdn.microsoft.com/webservices. Although we haven't covered it here, a technology preview that covers Web services on the .NET platform was made available a while ago. The Microsoft Web Service Enhancements provide early support for the WS-Security, WS-Routing, and DIME specifications and lets you experiment with newer specifications from within .NET applications.

Resources

Категории