Chapter 11. Web Services

Read any book, paper, or magazine article about Microsoft .NET and you’ll encounter one term over and over: “XML Web services.” XML Web services, or simply "Chapter 11" as they are more often called, are the cornerstone of the Microsoft .NET initiative. They’re the key to Microsoft’s vision of a world in which computers talk to each other over the Web using HTTP and other universally supported protocols. And they’re the number one reason that the Microsoft .NET Framework exists in the first place—to make it as easy as humanly possible to build Web services and Web service clients.

A Web service is a different kind of Web application. It doesn’t have a user interface as does a traditional Web application. Instead, it exposes callable API functions, better known as Web methods, over the Internet. It’s not designed to serve end users as traditional Web applications are. It’s designed to provide services to other applications, be they Web applications, GUI applications, or even command-line applications. What kinds of services do Web services provide? That’s up to the implementer. A Web service could provide real-time stock quotes to interested parties. It could validate credit cards or provide current information about the weather. Like traditional applications, Web services are as diverse as their creators’ imaginations. Microsoft, Sun, IBM, and others foresee a world in which all sorts of interesting information is made available via Web services. To the extent that developers embrace that vision, Web services will one day be the backbone of a highly programmable Internet—an Internet that doesn’t just serve end users, but one that allows servers to communicate with each other and applications to be freed from the bonds of the platforms on which they run.

An application that speaks the language of Web services has access to a universe of services that is just now emerging. Already, companies all over the world are exposing content and business logic through Web services. As one of this chapter’s sample programs demonstrates, it’s easy to build a Web service client that takes city and state names as input and fetches satellite images of said cities, thanks to Microsoft TerraService, which is a front end to a massive database of satellite images, aerial photographs, and topo maps of much of Earth’s surface and is freely available to anyone who wants to use it. In the future, you’ll see applications that use Web services to check the status of overnight packages or display the soup of the day at your favorite restaurant. Web services have the potential to change the world as few technologies ever have. And Microsoft .NET will play a huge role in that change, primarily because the .NET Framework makes writing Web services and Web service clients so incredibly easy.

Web services are not the property of Microsoft. They’re an industry standard built on open protocols such as HTTP and the Simple Object Access Protocol (SOAP). Many of the Web services in operation today run on UNIX servers. You don’t need the .NET Framework to write Web services or Web service clients, but you want the framework because it makes writing Web services and Web service clients easy. A few button clicks in Visual Studio .NET creates a Web service and exposes Web methods to anyone that you provide a URL to. Creating a Web service client requires equally little effort. You don’t even have to use Visual Studio .NET. You can write powerful Web services with Notepad, which is precisely what we’ll do in this chapter to introduce the brave new world of Web services and applications that use them.

Web Services

A great place to begin an exploration of Web services is to define precisely what a Web service is. A Web service is an application that:

  • Runs on a Web server

  • Exposes Web methods to interested callers

  • Listens for HTTP requests representing commands to invoke Web methods

  • Executes Web methods and returns the results

Most Web services expect their Web methods to be invoked using HTTP requests containing SOAP messages. SOAP is an XML-based vocabulary for performing remote procedure calls using HTTP and other protocols. You can read all about it at http://www.w3.org/TR/SOAP. Suppose you write a Web service that publishes Web methods named Add and Subtract that callers can use to add and subtract simple integers. If the service’s URL is www.wintellect.com/calc.asmx, here’s how a client would invoke the Add method by transmitting a SOAP envelope in an HTTP request. This example adds 2 and 2:

POST /calc.asmx HTTP/1.1
Host: www.wintellect.com
Content-Type: text/xml; charset=utf-8
Content-Length: 338
SOAPAction: "http://tempuri.org/Add"

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd=http://www.w3.org/2001/XMLSchema
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <Add xmlns="http://tempuri.org/">
      <a>2</a>
      <b>2</b>
    </Add>
  </soap:Body>
</soap:Envelope>

And here’s how the Web service would respond:

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

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
  xmlns:xsd=http://www.w3.org/2001/XMLSchema
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <AddResponse xmlns="http://tempuri.org/">
      <AddResult>4</AddResult>
    </AddResponse>
  </soap:Body>
</soap:Envelope>

The Web service’s job is to parse the SOAP envelope containing the inputs, add 2 and 2, formulate a SOAP envelope containing the sum of 2 and 2, and return it to the client in the body of the HTTP response. This, at the most elemental level, is what Web services are all about.

Web services written with the .NET Framework also allow their Web methods to be invoked using ordinary HTTP GET and POST commands. The following GET command adds 2 and 2 by invoking the Web service’s Add method:

GET /calc.asmx/Add?a=2&b=2 HTTP/1.1
Host: www.wintellect.com

The Web service responds as follows:

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

<?xml version="1.0" encoding="utf-8"?>
<int xmlns="http://tempuri.org/">4</int>

Here’s a POST command that adds 2 and 2:

POST /calc.asmx/Add HTTP/1.1
Host: www.wintellect.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 7

a=2&b=2

And here’s the Web service’s response:

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

<?xml version="1.0" encoding="utf-8"?>
<int xmlns="http://tempuri.org/">4</int>

As you can imagine, the hard part of writing a Web service is parsing HTTP requests and generating HTTP responses. But as you’ll see in the next section and throughout the remainder of this chapter, the .NET Framework insulates developers from the low-level details of HTTP, SOAP, and XML and provides a high-level framework for writing Web services and Web service clients alike.

There are many ways to write Web services. You can write Web services by hand. You can use SOAP toolkits from Microsoft, IBM, and other companies. And you can use the .NET Framework. Because this book is about Microsoft .NET, this chapter is about the latter. Writing Web services with the .NET Framework offers two advantages over all the other methods:

  • The .NET Framework makes writing Web services extremely easy.

  • Web services written with the .NET Framework are managed applications, meaning you shouldn’t have to worry about memory leaks, stray pointers, and other maladies that bedevil programmers and cost more than their fair share of development time.

What does it take to write a Web service using the .NET Framework? I’m glad you asked, because that’s what the next section is about.

Your First Web Service

The ASMX file shown in Example 11-1 is a complete Web service. It implements two Web methods: Add and Subtract. Both take two integers as input and return an integer as well. Deploying the Web service is as simple as copying it to a directory on your Web server that is URL-addressable. If you put Calc.asmx in wwwroot, the Web service’s local URL is http://localhost/calc.asmx.

Calc.asmx demonstrates several important principles of Web service programming using the .NET Framework:

  • Web services are implemented in ASMX files. ASMX is a special file name extension registered to ASP.NET (specifically, to an ASP.NET HTTP handler) in Machine.config.

  • ASMX files begin with @ WebService directives. At a minimum, the directive must contain a Class attribute identifying the class that makes up the Web service.

  • Web service classes can be attributed with optional WebService attributes. The one in this example assigns the Web service a name and a description that show up in the HTML page generated when a user calls up Calc.asmx in his or her browser. The WebService attribute also supports a Namespace parameter that can be used to change the name of the XML namespace that scopes the Web service’s members.

  • Web methods are declared by tagging public methods in the Web service class with WebMethod attributes. You can build helper methods into a Web service—methods that are used internally by Web methods but that are not exposed as Web methods themselves—by omitting the attribute. The WebMethod attributes in Example 11-1 also assign descriptive text to their Web methods. You’ll learn more about Description and other WebMethod parameters in the section entitled “The WebMethod Attribute.”

  • HTTP, XML, and SOAP are hidden under the hood. You don’t have to deal with raw XML data or SOAP messages because the .NET Framework deals with them for you.

    Example 11-1. Calc Web service.

    Calc.asmx

    <%@ WebService Language="C#" Class="CalcService" %>
    
    using System;
    using System.Web.Services;
    
    [WebService (Name="Calculator Web Service",
        Description="Performs simple math over the Web")]
    class CalcService
    {
        [WebMethod (Description="Computes the sum of two integers")]
        public int Add (int a, int b)
        {
            return a + b;
        }
    
        [WebMethod
            (Description="Computes the difference between two integers")]
        public int Subtract (int a, int b)
        {
            return a - b;
        }
    }

Despite its brevity, Calc.asmx is a full-blown Web service when installed on a Web server outfitted with ASP.NET. Its Web methods can be invoked with SOAP, HTTP GET, and HTTP POST, and it’s capable of returning output in SOAP responses or simple XML wrappers. All we need now is a way to test it out. The .NET Framework lends a hand there too.

Testing a Web Service

How do you test an ASMX Web service? Simple: just call it up in your browser. To demonstrate, copy Calc.asmx to wwwroot and type

http://localhost/calc.asmx

in your browser’s address bar. You’ll be greeted with the screen shown in Figure 11-2. What happened? ASP.NET responded to the HTTP request for Calc.asmx by generating an HTML page that describes the Web service. The name and description in the ASMX file’s WebService attribute appear at the top of the page. Underneath is a list of Web methods that the service exposes, complete with the descriptions spelled out in the WebMethod attributes.

Calc.asmx as seen in Internet Explorer.
Figure 11-2. Calc.asmx as seen in Internet Explorer.

Like what you’ve seen so far? It gets better. Click “Add” near the top of the page, and ASP.NET displays a page that you can use to test the Add method (Figure 11-3). ASP.NET knows the method name and signature because it reads them from the metadata in the DLL it compiled from Calc.asmx. It even generates an HTML form that you can use to call the Add method with your choice of inputs. Type 2 and 2 into the “a” and “b” boxes and click Invoke. The XML returned by the Web method appears in a separate browser window (Figure 11-4).

Test page for the Add method.
Figure 11-3. Test page for the Add method.
XML returned by the Add method.
Figure 11-4. XML returned by the Add method.

The forms that ASP.NET generates on the fly from ASMX files enable you to test the Web services that you write without writing special clients to test them with. They also let you explore a Web service built with the .NET Framework simply by pointing your browser to it. For kicks, type the following URL into your browser’s address bar:

http://terraservice.net/terraservice.asmx

That’s the URL of the Microsoft TerraService, an ultra-cool Web service that provides a programmatic interface to a massive database of geographic data known as the Microsoft TerraServer. Don’t worry about the details just yet; you’ll be using TerraService to build a Web service client later in this chapter. But do notice how much you can learn about TerraService simply by viewing the page that ASP.NET generated for it.

Web Services and Code-Behind

You can use code-behind to move Web service classes out of ASMX files and into separately compiled DLLs. Example 11-5 shows how Calc.asmx looks after it’s modified to take advantage of code-behind. The ASMX file now contains just one statement. The class referenced in that statement is implemented in Calc.cs. The following command compiles Calc.cs into a DLL named Calc.dll:

csc /t:library calc.cs

Once compiled, the DLL must be placed in the application root’s bin subdirectory (for example, wwwrootin).

Example 11-5. Calc Web service with code-behind.

Calc.asmx

<%@ WebService Class="CalcService" %>

Calc.cs

using System;
using System.Web.Services;

[WebService (Name="Calculator Web Service",
    Description="Performs simple math over the Web")]
class CalcService
{
    [WebMethod (Description="Computes the sum of two integers")]
    public int Add (int a, int b)
    {
        return a + b;
    }

    [WebMethod
        (Description="Computes the difference between two integers")]
    public int Subtract (int a, int b)
    {
        return a - b;
    }
}

Code-behind offers the same benefits to Web services that it offers to Web pages: it catches compilation errors before the service is deployed, and it enables you to write Web services in languages that ASP.NET doesn’t natively support. For an example of ASP.NET code written in managed C++ and for a refresher on code-behind in general, turn back to Chapter 5.

The WebService Base Class

Very often when you see ASMX code samples, the Web service classes inside them derive from a class named WebService, as in

class CalcService : WebService
{
  ...
}

WebService belongs to the System.Web.Services namespace. It contributes properties named Application, Session, Context, Server, and User to derived classes, enabling a Web service to access the ASP.NET objects with the same names. If you don’t use these objects in your Web service—for example, if you don’t use application state or session state—you don’t need to derive from WebService either.

The WebMethod Attribute

The WebMethod attribute tags a method as a Web method. The .NET Framework automatically exposes such methods as Web methods when they’re implemented inside a Web service. WebMethod is capable of doing much more, however, than simply letting the framework know which methods are Web methods and which are not; it also supports the following parameters:

Parameter Name

Description

BufferResponse

Enables and disables response buffering

CacheDuration

Caches responses generated by this method for the specified number of seconds

Description

Adds a textual description to a Web method

EnableSession

Enables and disables session state for this Web method

MessageName

Specifies the Web method’s name

TransactionOption

Specifies the transactional behavior of a Web method

CacheDuration is the ASMX equivalent of an @ OutputCache directive in an ASPX or ASCX file: it caches a method’s output so that subsequent requests will execute more quickly. Suppose, for example, that you write a Web method that returns the current time:

[WebMethod]
public string GetCurrentTime ()
{
    return DateTime.Now.ToShortTimeString ();
}

Since ToShortTimeString returns a string that includes minutes but not seconds, it’s wasteful to execute it too often. The following method declaration uses CacheDuration to cache the output for 10 seconds at a time:

[WebMethod (CacheDuration="10")]
public string GetCurrentTime ()
{
    return DateTime.Now.ToShortTimeString ();
}

Now the data that the method returns could be stale by a few seconds, but if the Web service is getting pounded with calls to GetCurrentTime, the load on it will be reduced commensurately.

Web services enjoy access to the same session state facilities that conventional ASP.NET applications do. By default, however, session state is disabled for Web methods. You can enable it with WebMethod’s EnableSession parameter. If you want to use session state in a Web service, derive from WebService (to inherit its Session property) and tag each Web method that uses session state with EnableSession=“true”:

class CalcService : WebService
{
    [WebMethod (EnableSession="true",
        Description="Adds an item to a shopping cart")]
    public void AddToCart (Item item)
    {
        ShoppingCart cart = (ShoppingCart) Session["MyShoppingCart"];
        cart.Add (item);
    }
}

Session state utilization is less common in Web services than in conventional Web applications, but it is an option nonetheless.

The MessageName parameter lets you assign a Web method a name other than that of the method that implements it. For example, suppose that you build two Add methods into a Web service—one that adds integers and another that adds floating point values—and you tag both of them as Web methods:

[WebMethod]
public int Add (int a, int b)
{
    return a + b;
}

[WebMethod]
public float Add (float a, float b)
{
    return a + b;
}

The only problem with this code is that it doesn’t compile. C# methods can be overloaded, but Web methods cannot. The solution? Either change the method names or add MessageName parameters to the WebMethod attributes, as demonstrated here:

[WebMethod (MessageName="AddInts")]
public int Add (int a, int b)
{
    return a + b;
}

[WebMethod (MessageName="AddFloats")]
public float Add (float a, float b)
{
    return a + b;
}

Now the C# methods remain overloaded, but the corresponding Web methods are named AddInts and AddFloats.

The Web Services Description Language

If other developers are to consume (that is, write clients for) a Web service that you author, they need to know what Web methods your service publishes, what protocols it supports, the signatures of its methods, and the Web service’s location (URL), among other things. All this information and more can be expressed in a language called the Web Services Description Language, or WSDL for short.

WSDL is a relatively new standard. It’s an XML vocabulary devised by IBM, Microsoft, and others. Its syntax is documented at http://www.w3.org/TR/wsdl. I won’t describe the details of the language here for several reasons. First, the details are already documented in the spec. Second, WSDL is a language for machines, not humans. Third, it’s trivial to generate a WSDL contract for a Web service built with the .NET Framework: simply point your browser to the Web service’s URL and append a WSDL query string, as in

http://www.wintellect.com/calc.asmx?wsdl

Figure 11-6 shows the result. Scan through it and you’ll find a service element that describes the Web service; operation elements that document the “operations,” or Web methods, that the service supports; binding elements that document the protocols that the Web methods support; and other descriptive information.

WSDL contract for Calc.asmx.
Figure 11-6. WSDL contract for Calc.asmx.

When you publish a Web service, you should also publish a WSDL contract describing it. For a Web service built with the .NET Framework, the contract is usually nothing more than a URL with ?wsdl on the end. Other developers can use the contract to write clients for your Web service. Typically, they don’t read the contract themselves. Instead, they run it through a tool that generates a wrapper class containing all the elements needed to talk to a Web service. The .NET Framework SDK includes one such tool: it’s called Wsdl.exe. You’ll learn all about it later in this chapter when we turn our attention from Web services to Web service clients.

Web Services and Complex Data Types

It’s not hard to understand how simple data types can be passed to and from Web methods. After all, integers and other primitive types are defined in one form or another on virtually every platform. But what about more complex types? What if, for example, you define a custom class or struct and want to use it as an input parameter or return value for a Web method? Are complex types supported, and if so, how do you declare them so that they become an intrinsic part of the Web service?

Complex types are supported, and they work very well because virtually any type can be represented in XML. As an example, consider the Web service in Example 11-7. It exposes a Web method named FindStores that accepts a state abbreviation (for example, “CA”) as input. FindStores calls a local method named FindByState, which queries the Pubs database that comes with Microsoft SQL Server for all the bookstores in the specified state and returns the results in an array of Bookstore objects. (Observe that FindByState is not a Web method because it lacks a WebMethod attribute.) FindStores returns the array to the client. Bookstore is a custom type defined in the ASMX file.

Figure 11-8 shows the XML returned when FindStores is called with the input string “CA”. The array of Bookstore objects has been serialized into XML. The serialization is performed by the .NET Framework’s System.Xml.Serialization.XmlSerializer class, otherwise known as the “XML serializer.” A client application that receives the XML and that has a schema describing the structure and content of the data can rehydrate the information into Bookstore objects. Or it can take the raw XML and do with it as it pleases.

Example 11-7. Bookstore locator Web service.

Locator.asmx

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

using System;
using System.Web.Services;
using System.Data;
using System.Data.SqlClient;

[WebService (Name="Bookstore Locator Service",
    Description="Retrieves bookstore information from the Pubs database")]
class LocatorService
{
    [WebMethod (Description="Finds bookstores in a specified state")]
    public Bookstore[] FindStores (string state)
    {
        return FindByState (state);
    }

    Bookstore[] FindByState (string state)
    {
        SqlDataAdapter adapter = new SqlDataAdapter
            ("select * from stores where state = ’" + state + "’",
            "server=localhost;database=pubs;uid=sa;pwd=");
        DataSet ds = new DataSet ();
        adapter.Fill (ds);

        DataTable table = ds.Tables[0];
        Bookstore[] stores = new Bookstore[table.Rows.Count];

        for (int i=0; i<table.Rows.Count; i++) {
            stores[i] = new Bookstore (
                table.Rows[i]["stor_name"].ToString ().TrimEnd
                    (new char[] { ’ ’ }),
                table.Rows[i]["stor_address"].ToString ().TrimEnd
                    (new char[] { ’ ’ }),
                table.Rows[i]["city"].ToString ().TrimEnd
                    (new char[] { ’ ’ }),
                table.Rows[i]["state"].ToString ().TrimEnd
                    (new char[] { ’ ’ })
            );
        }
        return stores;
    }
}

public class Bookstore
{
    public string Name;
    public string Address;
    public string City;
    public string State;

    public Bookstore () {}

    public Bookstore (string name, string address, string city,
        string state)
    {
        Name = name;
        Address = address;
        City = city;
        State = state;
    }
}
XML returned by the FindStores method.
Figure 11-8. XML returned by the FindStores method.

Where might a client obtain an XML schema describing the Bookstore data type? From the service’s WSDL contract, of course. Sneak a peek at Locator.asmx’s WSDL contract and you’ll see the Bookstore data type (and arrays of Bookstores) defined this way in the contract’s types element:

<s:complexType name="ArrayOfBookstore">
  <s:sequence>
    <s:element minOccurs="0" maxOccurs="unbounded"
      name="Bookstore" nillable="true" type="s0:Bookstore" /> 
  </s:sequence>
</s:complexType>
<s:complexType name="Bookstore">
  <s:sequence>
    <s:element minOccurs="1" maxOccurs="1" name="Name"
      nillable="true" type="s:string" /> 
    <s:element minOccurs="1" maxOccurs="1" name="Address"
      nillable="true" type="s:string" /> 
    <s:element minOccurs="1" maxOccurs="1" name="City"
      nillable="true" type="s:string" /> 
    <s:element minOccurs="1" maxOccurs="1" name="State"
      nillable="true" type="s:string" /> 
  </s:sequence>
</s:complexType>

Given these definitions, a client can define a Bookstore class of its own and initialize arrays of Bookstore objects by deserializing Bookstore elements. It’s not as hard as it sounds. If the client is written with the .NET Framework, tools generate the class definitions for you and the framework handles the deserialization.

As Locator.asmx demonstrates, it’s not difficult to write Web methods that use custom types. There are, however, two gotchas to be aware of:

  • Because query strings are limited to passing simple name/value pairs, you can’t pass complex types to a Web method using HTTP GET and POST. That’s not a limitation if you use SOAP to invoke Web methods, but it does prevent ASP.NET from generating test pages for methods that accept complex types. If you go to a test page and see the warning “No test form is available because this method does not support HTTP GET” or something to that effect, you’ve found a method that accepts an input parameter that can’t be represented in a query string. ASP.NET test forms invoke methods using HTTP GET commands.

  • Any fields or properties declared in a class or struct that’s passed to or from a Web method must be public if they’re to be serialized when instances of the class or struct are serialized. That’s because the .NET Framework’s XML serializer will not serialize nonpublic members.

Keep these caveats in mind and you’ll have few problems combining Web methods and custom data types.

Web Service Discovery—DISCO

Once a client has a WSDL contract describing a Web service, it has all the information it needs to make calls to that Web service. But when you publish a Web service by making it available on a Web server, how do clients find out where to get a WSDL contract? For that matter, how do clients know that your Web service exists in the first place?

The answer comes in two parts: DISCO and Universal Description, Discovery, and Integration, better known as UDDI. The former is a file-based mechanism for local Web service discovery—that is, for getting a list of available Web services from DISCO files deployed on Web servers. The latter is a global Web service directory that is itself implemented as a Web service. UDDI is discussed in the next section.

The DISCO (short for “discovery”) protocol is a simple one that revolves around XML-based DISCO files. The basic idea is that you publish a DISCO file on your Web server that describes the Web services available on it and perhaps on other servers as well. Clients can interrogate the DISCO file to find out what Web services are available and where the services’ WSDL contracts can be found. As an example, suppose you publish two Web services and their URLs are as follows:

  • http://www.wintellect.com/calc.asmx

  • http://www.wintellect.com/locator.asmx

To advertise these Web services, you can deploy the following DISCO file at a well-known URL on your server. The contractRef elements identify the URLs of the Web services’ WSDL contracts. URLs can be absolute or relative (relative to the directory in which the DISCO file resides). The optional docRef attributes identify the locations of documents describing the Web services, which, because of the self-documenting nature of Web services built with the .NET Framework, are typically the ASMX files themselves:

<?xml version="1.0" ?>
<discovery xmlns="http://schemas.xmlsoap.org/disco/"
  xmlns:scl="http://schemas.xmlsoap.org/disco/scl/">
  <scl:contractRef ref="http://www.wintellect.com/calc.asmx?wsdl"
    docRef="http://www.wintellect.com/Calc.asmx" />
  <scl:contractRef ref="http://www.wintellect.com/locator.asmx?wsdl"
    docRef="http://www.wintellect.com/Locator.asmx" />
</discovery>

If you’d prefer, you can write DISCO files for individual Web services and reference them in a master DISCO file using discoveryRef elements. Here’s a DISCO file that points to other DISCO files. Once more, URLs can be absolute or relative:

<?xml version="1.0" ?>
<discovery xmlns="http://schemas.xmlsoap.org/disco/">
  <discoveryRef ref="http://www.wintellect.com/calc.disco" />
  <discoveryRef ref="http://www.wintellect.com/locator.disco" />
</discovery>

A third option is to deploy a VSDISCO file to enable dynamic discovery. The following VSDISCO file automatically exposes all ASMX and DISCO files in a host directory and its subdirectories, with the exception of those subdirectories noted with exclude elements:

<?xml version="1.0" ?>
<dynamicDiscovery
  xmlns="urn:schemas-dynamicdiscovery:disco.2000-03-17">
  <exclude path="_vti_cnf" />
  <exclude path="_vti_pvt" />
  <exclude path="_vti_log" />
  <exclude path="_vti_script" />
  <exclude path="_vti_txt" />
</dynamicDiscovery>

How does dynamic discovery work? ASP.NET maps the file name extension .vsdisco to an HTTP handler that scans the host directory and subdirectories for ASMX and DISCO files and returns a dynamically generated DISCO document. A client that requests a VSDISCO file gets back what appears to be a static DISCO document.

For security reasons, Microsoft disabled dynamic discovery just before version 1.0 of the .NET Framework shipped. You can reenable it by uncommenting the line in the httpHandlers section of Machine.config that maps *.vsdisco to System.Web.Services.Discovery.DiscoveryRequestHandler and granting the ASPNET account permission to access the IIS metabase. Microsoft highly discourages dynamic discovery for fear of compromising your Web server, and a bug in version 1.0 of the .NET Framework SDK prevents most DISCO-aware tools from working with VSDISCO anyway. My advice is to forget that VSDISCO files even exist and use static DISCO files instead.

To further simplify Web service discovery, you can link to a master DISCO file from your site’s default HTML document. For example, suppose the default HTML document at www.wintellect.com is Default.html and that the same directory also holds a discovery document named Default.disco. Including the following HTML in Default.html enables most tools that read DISCO files to accept the URL www.wintellect.com (as opposed to www.wintellect.com/default.disco):

<html>
  <head>
    <link type="text/html" rel="alternate" href="Default.disco">
  </head>
</html>

Visual Studio .NET (specifically, its Add Web Reference command) reads DISCO files; so does the Disco.exe utility that comes with the .NET Framework SDK.

DISCO’s chief disadvantage is that you can’t read a DISCO file if you don’t have its URL. So how do you find a Web service if you don’t even have a URL to start with? Can you spell U-D-D-I?

Web Service Discovery—UDDI

UDDI is an abbreviation for Universal Description, Discovery, and Integration. Jointly developed by IBM, Microsoft, and Ariba and supported by hundreds of other companies, UDDI is a specification for building distributed databases that enable interested parties to “discover” each other’s Web services. No one company owns the databases; anyone is free to publish a UDDI-based business registry. Operator sites have already been established by IBM and Microsoft and are likely to be the first of many such sites that will come on line in the future.

UDDI sites are themselves Web services. They publish a pair of SOAP-based APIs: an inquiry API for inquiring about companies and their Web services and a publisher API for advertising a company’s Web services. Anyone can call the inquiry API, but operator sites typically limit the publisher API to registered members. Documentation regarding these APIs and other information about UDDI can be found at http://www.uddi.org. At the time of this writing, Microsoft was beta testing a UDDI .NET SDK featuring managed wrapper classes that simplify interactions with UDDI business registries. For the latest information on this and other UDDI development tools proffered by Microsoft, visit http://uddi.microsoft.com/default.aspx.

Most developers will never deal with UDDI APIs directly. Instead, they’ll use high-level tools such as Visual Studio .NET to query UDDI business registries and generate wrapper classes that allow them to place calls to the Web services that they find there. The actual placing of UDDI calls will be limited primarily to tools vendors and to clients that wish to locate and bind to Web services dynamically.

Web Service Clients

Now that you’ve seen Web services up close and personal, it’s time to learn about Web service clients—that is, applications that use, or consume, Web methods. It’s easy to write Web services. Writing Web service clients is even easier, thanks to some high-level support lent by the .NET Framework class library (FCL) and a code-generator named Wsdl.exe. If you have a WSDL contract describing a Web service (or the URL of a DISCO file that points to a WSDL contract), you can be making calls to that Web service in no time.

Web Service Proxies

The key concept to grasp when writing Web service clients is that of the Web service proxy. A Web service proxy is an object that provides a local representation of a remote Web service. A proxy is instantiated in the client’s own application domain, but calls to the proxy flow through the proxy and out to the Web service that the proxy represents. The Wsdl.exe utility that comes with the .NET Framework SDK (and that is integrated into Visual Studio .NET) generates Web service proxy classes from WSDL contracts. Once a proxy is created, calling the corresponding Web service is a simple matter of calling methods on the proxy, as shown here:

CalculatorWebService calc = new CalculatorWebService ();
int sum = calc.Add (2, 2);

The methods in the proxy class mirror the Web methods in the Web service. If the Web service exposes Web methods named Add and Subtract, the Web service proxy also contains methods named Add and Subtract. When you call one of these methods, the proxy packages up the input parameters and invokes the Web method using the protocol encapsulated in the proxy (typically SOAP). The proxy insulates you from the low-level details of the Web service and of the protocols that it uses. It even parses the XML that comes back and makes the result available as managed types.

Using Wsdl.exe to generate a Web service proxy is simplicity itself. Suppose you want to call a Web service whose URL is http://www.wintellect.com/calc.asmx. If the Web service was written with the .NET Framework, which means you can retrieve a WSDL contract by appending a ?wsdl query string to the service URL, you can generate a proxy for the Web service like this:

wsdl http://www.wintellect.com/calc.asmx?wsdl

Or you can leave off the query string and let Wsdl.exe supply it for you:

wsdl http://www.wintellect.com/calc.asmx

If Calc.asmx wasn’t written with the .NET Framework, it might not support WSDL query strings. In that case, you find the WSDL contract and pass its URL (or local path name) to Wsdl.exe. The following example assumes that the contract is stored in a local file named Calc.wsdl:

wsdl calc.wsdl

However you point it to the WSDL contract, Wsdl.exe generates a CS file containing a class that represents the Web service proxy. That’s the class you instantiate to invoke the Web service’s methods.

The proxy class’s name comes from the service name (that is, the name attribute accompanying the service element) in the WSDL contract. For example, suppose you attribute a Web service as follows in its ASMX file:

[WebService (Name="Calculator Web Service")]

The resulting <service> tag in the WSDL contract looks like this:

<service name="Calculator Web Service">

and the resulting proxy class is named CalculatorWebService. By default, the name of the CS file that Wsdl.exe generates also derives from the service name (for example, Calculator Web Service.cs). You can override that name by passing Wsdl.exe a /out switch. The command

wsdl /out:Calc.cs http://www.wintellect.com/calc.asmx

names the output file Calc.cs regardless of the service name.

Wsdl.exe supports a number of command line switches that you can use to customize its output. For example, if you’d prefer the proxy class to be written in Visual Basic .NET rather than C#, use the /language switch:

wsdl /language:vb http://www.wintellect.com/calc.asmx

If you’d like Wsdl.exe to enclose the code that it generates in a namespace (which is extremely useful for preventing collisions between types defined in the generated code and types defined in your application and in the FCL), use the /namespace switch:

wsdl /namespace:Calc http://www.wintellect.com/calc.asmx

Classes generated by Wsdl.exe derive from base classes in the FCL’s System.Web.Services.Protocols namespace. By default, a proxy class derives from SoapHttpClientProtocol, which enables it to invoke Web methods using SOAP over HTTP. You can change the invocation protocol with Wsdl.exe’s /protocol switch. The command

wsdl /protocol:httpget http://www.wintellect.com/calc.asmx

creates a Web service proxy that derives from HttpGetClientProtocol and calls Web methods using HTTP GET commands, while the command

wsdl /protocol:httppost http://www.wintellect.com/calc.asmx

creates a proxy that derives from HttpPostClientProtocol and uses HTTP POST. Why would you want to change the protocol that a proxy uses to invoke Web methods? In the vast majority of cases, SOAP is fine. However, if the methods that you’re calling are simple methods that use equally simple data types, switching to HTTP GET or POST makes calls slightly more efficient by reducing the amount of data transmitted over the wire.

Incidentally, if you use Visual Studio .NET to write Web service clients, you don’t have to run Wsdl.exe manually. When you use the Add Web Reference command found in the Project menu, Visual Studio .NET runs Wsdl.exe for you and adds the proxy class to your project. Add Web Reference also speaks the language of UDDI, making it easy to search Microsoft’s UDDI registry for interesting Web services.

A Simple Web Service Client

Want to write a client for Calc.asmx? Here are the steps:

  1. Use Wsdl.exe to create a proxy class for Calc.asmx. If you installed Calc.asmx in wwwroot, the proper command is

    wsdl http://localhost/calc.asmx

    Wsdl.exe responds by creating a file named Calculator Web Service.cs.

  2. Create a new text file named CalcClient.cs and enter the code in Example 11-9.

  3. Compile the CS files into a console application with the following command:

    csc CalcClient.cs "Calculator Web Service.cs"
  4. Run CalcClient.exe.

    CalcClient.exe instantiates a Web service proxy and calls the service’s Add method. The resulting output proves beyond the shadow of a doubt that Calc.asmx is smart enough to add 2 and 2 (Figure 11-10).

    Example 11-9. Console client for Calc.asmx.

    CalcClient.cs

    using System;
    
    class MyApp
    {
        public static void Main ()
        {
            CalculatorWebService calc = new CalculatorWebService ();
            int sum = calc.Add (2, 2);
            Console.WriteLine ("2 + 2 = " + sum);
        }
    }
    Output from CalcClient.exe.
    Figure 11-10. Output from CalcClient.exe.

Avoiding Hard-Coded Service URLs

Look through a CS file generated by Wsdl.exe, and you’ll see the Web service proxy class as well as the methods that wrap the Web service’s Web methods. You’ll also see that the Web service’s URL is hardcoded into the CS file in the proxy’s class constructor. Here’s an example:

public CalculatorWebService() {
    this.Url = "http://www.wintellect.com/calc.asmx";
}

If the Web service moves, you’ll have to modify the CS file and regenerate the proxy.

To avoid having to update code when a Web service’s URL changes, you can use Wsdl.exe’s /appsettingurlkey (abbreviated /urlkey) switch. The command

wsdl /urlkey:CalcUrl http://www.wintellect.com/calc.asmx

produces the following class constructor:

public CalculatorWebService() {
    string urlSetting =
System.Configuration.ConfigurationSettings.AppSettings["CalcUrl"];
    if ((urlSetting != null)) {
        this.Url = urlSetting;
    }
    else {
        this.Url = "http://www.wintellect.com/calc.asmx";
    }
}

Now you can assign a value to “CalcUrl” in the appSettings section of a local Web.config file, like so:

<configuration>
  <appSettings>
    <add key="CalcUrl" value="http://www.wintellect.com/calc.asmx" />
  </appSettings>
</configuration>

If the URL changes, you can update the proxy simply by editing Web.config. No code changes are required.

Asynchronous Method Calls

Something else you’ll notice if you open a CS file generated by Wsdl.exe is that the proxy class contains asynchronous as well as synchronous wrappers around the Web service’s methods. The former can be used to invoke Web methods asynchronously. An asynchronous call returns immediately, no matter how long the Web service requires to process the call. To retrieve the results from an asynchronous call, you make a separate call later on.

Here’s an example using Calc.asmx’s Add method that demonstrates how to invoke a Web method asynchronously. The client calls the proxy’s BeginAdd method to initiate an asynchronous call and then goes off to attend to other business. Later it returns to finish the call by calling EndAdd:

CalculatorWebService calc = new CalculatorWebService ();
IAsyncResult res = calc.BeginAdd (2, 2, null, null);
  .
  .
  .
int sum = calc.EndAdd (res);

If the call hasn’t completed when EndAdd is called, EndAdd blocks until it does. If desired, a client can use the IsCompleted property of the IAsyncResult interface returned by BeginAdd to determine whether the call has completed and avoid calling EndAdd prematurely:

IAsyncResult res = calc.BeginAdd (2, 2, null, null);
  .
  .
  .
if (res.IsCompleted) {
    int sum = calc.EndAdd (res);
}
else {
    // Try again later
}

Another option is to ask to be notified when an asynchronous call returns by providing a reference to an AsyncCallback delegate wrapping a callback method. In the next example, EndAdd won’t block because it isn’t called until the client is certain the method call has returned:

AsyncCallback cb = new AsyncCallback (AddCompleted);
IAsyncResult res = calc.BeginAdd (2, 2, cb, null);
  .
  .
  .
public void AddCompleted (IAsyncResult res)
{
    int sum = calc.EndAdd (res);
}

Whatever approach you decide on, the proxy’s asynchronous method–call support is extraordinarily useful for calling methods that take a long time to complete. Add isn’t a very realistic example because it’s such a simple method, but the principle is valid nonetheless.

Web Service Clients and Proxy Servers

If a client invokes methods on a Web service from behind a proxy server, the Web service proxy needs to know the address of the proxy server. You can provide that address in two ways. The first option is to pass Wsdl.exe a /proxy switch specifying the proxy server’s URL:

wsdl /proxy:http://myproxy http://www.wintellect.com/calc.asmx

Option number two is to programmatically initialize the Web service proxy’s Proxy property (which it inherits from HttpWebClientProtocol) with a reference to a WebProxy object (System.Net.WebProxy) identifying the proxy server:

CalculatorWebService calc = new CalculatorWebService ();
calc.Proxy = new WebProxy (http://myproxy, true);
int sum = calc.Add (2, 2);

The true passed to WebProxy’s constructor bypasses the proxy server for local addresses. Pass false instead to route all requests through the proxy server.

The CityView Application

The Web application pictured in Figure 11-11—CityView—is a novel and graphic example of a Web service client. The Web service that it connects to is the Microsoft TerraService (http://terraservice.net/terraservice.asmx), which is a Web service front end to the Microsoft TerraServer database. You can read all about TerraServer and TerraService at http://terraservice.net. TerraServer is one of the world’s largest online databases. Inside it are photographs and maps of much of Earth’s surface, made available to the public through a partnership between Microsoft and the U.S. Geological Survey. TerraService exposes TerraServer’s content via Web methods. There are 16 Web methods in all, with names such as ConvertPlaceToLonLatPt and GetTile. As you might expect, TerraService was written with the Microsoft .NET Framework. Its WSDL contract is available at http://terraservice.net/terraservice.asmx?wsdl.

Before you can run CityView, you need to install it on your Web server. Here’s how:

  1. Copy CityView.aspx and CityView.ashx from the CD that came with this book to the virtual directory of your choice.

  2. Create a subdirectory named bin in the directory that you copied CityView.aspx and CityView.ashx to. Then copy TerraService.dll to the bin directory. TerraService.dll is a DLL containing a TerraService proxy class named TerraService.

CityView showing an aerial view of San Francisco.
Figure 11-11. CityView showing an aerial view of San Francisco.

Now that CityView is installed, try it out by calling up CityView.aspx in your browser. Enter a city name (for example, “New York”) and pick a state. Then click Show Image. After a brief pause, the specified city appears at the bottom of the page. If CityView is unable to fetch the image you requested, it responds by displaying “Image not available” in place of the image. That could mean you entered a city that doesn’t exist. Or it could mean that TerraService is temporarily down or your Internet connection is taking a nap.

You can zoom in and out by selecting different scales. The default scale is 8 meters. Choose a smaller number to zoom in or a larger number to zoom out. For a great aerial view of the San Francisco peninsula that includes an overhead shot of the Golden Gate Bridge, enter San Francisco, CA and choose a scale of 32 meters.

How CityView Works

CityView consists of three files:

  • CityView.aspx

  • CityView.ashx

  • TerraService.dll

CityView.aspx is a Web form that defines CityView’s user interface. Its source code appears in Example 11-12. The user interface consists of a TextBox for typing city names, a DropDownList for selecting states, a RadioButtonList for choosing scales, and a Button for posting back to the server and fetching images. It also includes an Image control whose ImageUrl property is programmatically initialized following each postback. Here’s the code that assigns a URL to the Image control:

MyImage.ImageUrl = builder.ToString ();

If you enter Redmond, WA, and accept the default scale of 8 meters, the string assigned to ImageURL looks like this:

CityView.ashx?city=Redmond&state=WA&scale=8

which sets the stage perfectly for a discussion of the second component of CityView: namely, CityView.ashx.

CityView.ashx is an HTTP handler. Specifically, it’s an HTTP handler that generates and returns a bitmap image of the location named in a query string. When we deployed an HTTP handler in Chapter 8, we coded the handler in a CS file, compiled it into a DLL, and dropped the DLL into the application root’s bin directory. We also registered the handler using a Web.config file. CityView.ashx demonstrates the other way to deploy HTTP handlers. You simply code an IHttpHandler-derived class into an ASHX file and include an @ WebHandler directive that identifies the class name and the language in which the class is written:

<%@ WebHandler Language="C#" Class="CityViewImageGen" %>

When a client requests an ASHX file containing a WebHandler class, ASP.NET compiles the class for you. The beauty of deploying an HTTP handler in an ASHX file is that you don’t have to register the handler in a CONFIG file or in the IIS metabase; you just copy the ASHX file to your Web server. The downside, of course, is that you must test the handler carefully to make sure ASP.NET can compile it.

The CityViewImageGen class inside CityView.ashx (Example 11-12) generates the images that CityView.aspx displays. Its heart is the ProcessRequest method, which is called on each and every request. ProcessRequest calls a local method named GetTiledImage to generate the image. Then it returns the image in the HTTP response by calling Save on the Bitmap object encapsulating the image:

bitmap.Save (context.Response.OutputStream, format);

Should GetTiledImage fail, ProcessRequest returns a simple bitmap containing the words “Image not available” instead of an aerial photograph. It also adjusts the format of the bitmap to best fit the type of content returned: JPEG for photographs, and GIF for bitmaps containing text.

GetTiledImage uses three TerraService Web methods:

  • ConvertPlaceToLonLatPt, which converts a “place” (city, state, country) into a latitude and longitude

  • GetAreaFromPt, which takes a latitude and longitude and an image size (in pixels) and returns an AreaBoundingBox representing the image boundaries

  • GetTile, which takes a tile ID (obtained from the AreaBoundingBox) and returns the corresponding tile

A “tile” is a 200-pixel-square image of a particular geographic location. To build larger images, a TerraService client must fetch multiple tiles and stitch them together to form a composite. That’s how GetTiledImage generates the 600 x 400 images that it returns. It starts by creating a Bitmap object to represent the image. Then it uses Graphics.DrawImage to draw each tile onto the image. The logic is wholly independent of the image size, so if you’d like to modify CityView to show larger (or smaller) images, find the statement

Bitmap bitmap = GetTiledImage (city, state, res, 600, 400);

in CityView.ashx and change the 600 and 400 to the desired width and height.

The third and final component of CityView is TerraService.dll, which contains the TerraService proxy class named TerraService. CityView.ashx’s GetTiledImage method instantiates the proxy class and uses the resulting object to call TerraService’s Web methods:

TS.TerraService ts = new TS.TerraService ();

TerraService.dll was compiled from TerraService.cs, which I generated with the following command:

wsdl /namespace:TS http://terraservice.net/terraservice.asmx

The namespace was necessary to prevent certain data types defined in TerraService’s WSDL contract from conflicting with data types defined in the .NET Framework Class Library. Once TerraService.cs was created, the command

csc /t:library terraservice.cs

compiled it into a DLL.

Example 11-12. CityView source code.

CityView.aspx

<html>
  <body>
    <h1>CityView</h1>
    <hr>
    <form runat="server">
      <table cellpadding="8">
        <tr>
          <td>
            City
          </td>
          <td>
            <asp:TextBox ID="City" Width="100%" RunAt="server" />
          </td>
          <td>
            <asp:RequiredFieldValidator
              ControlToValidate="City"
              ErrorMessage="*"
              Display="static"
              Color="red"
              RunAt="server"
            />
          </td>
        </tr>
        <tr>
          <td>
            State
          </td>
          <td>
            <asp:DropDownList ID="State" Width="100%" RunAt="server">
              <asp:ListItem Text="AL" RunAt="server" />
              <asp:ListItem Text="AK" RunAt="server" />
              <asp:ListItem Text="AR" RunAt="server" />
              <asp:ListItem Text="AZ" RunAt="server" />
              <asp:ListItem Text="CA" RunAt="server" />
              <asp:ListItem Text="CO" RunAt="server" />
              <asp:ListItem Text="CT" RunAt="server" />
              <asp:ListItem Text="DC" RunAt="server" />
              <asp:ListItem Text="DE" RunAt="server" />
              <asp:ListItem Text="FL" RunAt="server" />
              <asp:ListItem Text="GA" RunAt="server" />
              <asp:ListItem Text="HI" RunAt="server" />
              <asp:ListItem Text="IA" RunAt="server" />
              <asp:ListItem Text="ID" RunAt="server" />
              <asp:ListItem Text="IL" RunAt="server" />
              <asp:ListItem Text="IN" RunAt="server" />
              <asp:ListItem Text="KS" RunAt="server" />
              <asp:ListItem Text="KY" RunAt="server" />
              <asp:ListItem Text="LA" RunAt="server" />
              <asp:ListItem Text="MA" RunAt="server" />
              <asp:ListItem Text="MD" RunAt="server" />
              <asp:ListItem Text="ME" RunAt="server" />
              <asp:ListItem Text="MI" RunAt="server" />
              <asp:ListItem Text="MN" RunAt="server" />
              <asp:ListItem Text="MO" RunAt="server" />
              <asp:ListItem Text="MS" RunAt="server" />
              <asp:ListItem Text="MT" RunAt="server" />
              <asp:ListItem Text="NC" RunAt="server" />
              <asp:ListItem Text="ND" RunAt="server" />
              <asp:ListItem Text="NE" RunAt="server" />
              <asp:ListItem Text="NH" RunAt="server" />
              <asp:ListItem Text="NJ" RunAt="server" />
              <asp:ListItem Text="NM" RunAt="server" />
              <asp:ListItem Text="NV" RunAt="server" />
              <asp:ListItem Text="NY" RunAt="server" />
              <asp:ListItem Text="OH" RunAt="server" />
              <asp:ListItem Text="OK" RunAt="server" />
              <asp:ListItem Text="OR" RunAt="server" />
              <asp:ListItem Text="PA" RunAt="server" />
              <asp:ListItem Text="RI" RunAt="server" />
              <asp:ListItem Text="SC" RunAt="server" />
              <asp:ListItem Text="SD" RunAt="server" />
              <asp:ListItem Text="TN" RunAt="server" />
              <asp:ListItem Text="TX" RunAt="server" />
              <asp:ListItem Text="UT" RunAt="server" />
              <asp:ListItem Text="VA" RunAt="server" />
              <asp:ListItem Text="VT" RunAt="server" />
              <asp:ListItem Text="WA" RunAt="server" />
              <asp:ListItem Text="WI" RunAt="server" />
              <asp:ListItem Text="WV" RunAt="server" />
              <asp:ListItem Text="WY" RunAt="server" />
            </asp:DropDownList>
          </td>
          <td>
          </td>
        </tr>
        <tr>
          <td>
          </td>
          <td>
            <fieldset>
              <legend>Scale</legend>
              <asp:RadioButtonList ID="Scale" RunAt="server"
                RepeatColumns="2" RepeatDirection="Horizontal">
                <asp:ListItem Text="1 meter" RunAt="server" />
                <asp:ListItem Text="2 meters" RunAt="server" />
                <asp:ListItem Text="4 meters" RunAt="server" />
                <asp:ListItem Text="8 meters" Selected="true"
                  RunAt="server" />
                <asp:ListItem Text="16 meters" RunAt="server" />
                <asp:ListItem Text="32 meters" RunAt="server" />
              </asp:RadioButtonList>
            </fieldset>
          </td>
          <td>
          </td>
        </tr>
        <tr>
          <td>
          </td>
          <td>
            <asp:Button Text="Show Image" OnClick="OnShowImage"
              Width="100%" RunAt="server" />
          </td>
          <td>
          </td>
        </tr>
      </table>
    </form>
    <hr>
    <asp:Image ID="MyImage" RunAt="server" />
  </body>
</html>

<script language="C#" runat="server">
  void OnShowImage (Object sender, EventArgs e)
  {
      StringBuilder builder = new StringBuilder ();
      builder.Append ("CityView.ashx?city=");
      builder.Append (City.Text);
      builder.Append ("&state=");
      builder.Append (State.SelectedItem.Text);
      builder.Append ("&scale=");

      switch (Scale.SelectedIndex) {
      case 0:
          builder.Append ("1");
          break;

      case 1:
          builder.Append ("2");
          break;

      case 2:
          builder.Append ("4");
          break;

      case 3:
          builder.Append ("8");
          break;

      case 4:
          builder.Append ("16");
          break;

      case 5:
          builder.Append ("32");
          break;
      }

      MyImage.ImageUrl = builder.ToString ();
  }
</script>

CityView.ashx

<%@ WebHandler Language="C#" Class="CityViewImageGen" %>

using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

public class CityViewImageGen : IHttpHandler
{
    public void ProcessRequest (HttpContext context)
    {
        // Extract user input from the query string
        string city = context.Request["City"];
        string state = context.Request["State"];
        string scale = context.Request["Scale"];

        if (city != null && state != null) {
            // Determine the scale
            TS.Scale res = TS.Scale.Scale8m;

            if (scale!= null) {
                switch (scale) {
                case "1":
                    res = TS.Scale.Scale1m;
                    break;

                case "2":
                    res = TS.Scale.Scale2m;
                    break;

                case "4":
                    res = TS.Scale.Scale4m;
                    break;

                case "8":
                    res = TS.Scale.Scale8m;
                    break;

                case "16":
                    res = TS.Scale.Scale16m;
                    break;

                case "32":
                    res = TS.Scale.Scale32m;
                    break;
                }
            }

            // Generate the requested image
            string type = "image/jpeg";
            ImageFormat format = ImageFormat.Jpeg;
            Bitmap bitmap = GetTiledImage (city, state, res, 600, 400);

            // If GetTiledImage failed, generate an error bitmap
            if (bitmap == null) {
                bitmap = GetErrorImage ("Image not available");
                type = "image/gif";
                format = ImageFormat.Gif;
            }

            // Set the response’s content type
            context.Response.ContentType = type;

            // Write the image to the HTTP response
            bitmap.Save (context.Response.OutputStream, format);

            // Clean up and return
            bitmap.Dispose ();
        }
    }

    public bool IsReusable
    {
        get { return true; }
    }

    Bitmap GetTiledImage (string City, string State, TS.Scale Scale,
        int cx, int cy)
    {
        Bitmap bitmap = null;
        Graphics g = null;

        try {
            // Instantiate the TerraService proxy
            TS.TerraService ts = new TS.TerraService ();

            // Get the latitude and longitude of the requested city
            TS.Place place = new TS.Place ();
            place.City = City;
            place.State = State;
            place.Country = "USA";
            TS.LonLatPt point = ts.ConvertPlaceToLonLatPt (place);

            // Compute the parameters for a bounding box
            TS.AreaBoundingBox abb = ts.GetAreaFromPt (point,
                TS.Theme.Photo, Scale, cx, cy);

            // Create an image to fit the bounding box
            bitmap = new Bitmap (cx, cy,
                PixelFormat.Format32bppRgb);
            g = Graphics.FromImage (bitmap);

            int x1 = abb.NorthWest.TileMeta.Id.X;
            int y1 = abb.NorthWest.TileMeta.Id.Y;
            int x2 = abb.NorthEast.TileMeta.Id.X;
            int y2 = abb.SouthWest.TileMeta.Id.Y;

            for (int x=x1; x<=x2; x++) {
                for (int y=y1; y>=y2; y--) {
                    TS.TileId tid = abb.NorthWest.TileMeta.Id;
                    tid.X = x;
                    tid.Y = y;
                    Image tile = Image.FromStream
                        (new MemoryStream (ts.GetTile (tid)));
                    g.DrawImage (tile,
                        (x - x1) * tile.Width  -
                        (int) abb.NorthWest.Offset.XOffset,
                        (y1 - y) * tile.Height -
                        (int)abb.NorthWest.Offset.YOffset,
                        tile.Width, tile.Height);
                    tile.Dispose();
                }
            }

            // Return the image
            return bitmap;
        }
        catch (Exception) {
            if (bitmap != null)
                bitmap.Dispose ();
            return null;
        }
        finally {
            if (g != null)
                g.Dispose ();
        }
    }

    Bitmap GetErrorImage (string message)
    {
        // Determine the width and height of the error message
        Bitmap bitmap =
            new Bitmap (1, 1, PixelFormat.Format32bppRgb);
        Graphics g = Graphics.FromImage (bitmap);
        Font font = new Font ("Verdana", 10);
        SizeF size = g.MeasureString (message, font);
        int cx = (int) size.Width;
        int cy = (int) size.Height;
        bitmap.Dispose ();
        g.Dispose ();

        // Generate a bitmap containing the error message
        bitmap = new Bitmap (cx, cy, PixelFormat.Format32bppRgb);
        g = Graphics.FromImage (bitmap);
        SolidBrush redBrush = new SolidBrush (Color.Red);
        SolidBrush whiteBrush = new SolidBrush (Color.White);

        g.FillRectangle (whiteBrush, 0, 0, 256, 64);
        g.DrawString (message, font, redBrush, 0, 0);

        whiteBrush.Dispose ();
        redBrush.Dispose ();
        font.Dispose ();
        g.Dispose ();

        // Return the image
        return bitmap;
    }
}

For-Fee Web Services

One of the most commonly asked questions regarding Web services has to do with economics rather than programming conundrums. The question can be summarized as follows. I’m thinking about writing a particular Web service. For it to be commercially viable, I have to charge a fee for using it. If I charge for it, I have to know who’s calling it. How do I know who’s calling my Web service? In other words, how do I identify the originator of a Web method call?

The question is actually one of security. Identifying callers means authenticating callers. There are several ways to authenticate Web service callers. Here are three possibilities:

  • Assign authorized callers an authentication key (for example, a password or a random sequence of digits) and require them to pass the key in each method call.

  • Transmit user names and passwords in HTTP Authorization headers.

  • Transmit user names and passwords or some other form of credentials in SOAP headers.

The third of these three options is arguably the most compelling because it transmits authentication data out-of-band and enables developers to leverage the high-level support for SOAP headers built into the .NET Framework. To demonstrate, here’s a Web service whose Add method can be called successfully only if the SOAP request contains a header named AuthHeader that in turn contains a UserName element equal to “jeffpro” and a Password element equal to “imbatman”:

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

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;

public class AuthHeader : SoapHeader
{
    public string UserName;
    public string Password;
}

class SafeService
{
    public AuthHeader header;

    [WebMethod]
    [SoapHeader ("header", Required="true")]
    public int Add (int a, int b)
    {
        if (header.UserName == "jeffpro" &&
            header.Password == "imbatman")
            return a + b;
        else
            throw new HttpException (401, "Not authorized");
    }
}

AuthHeader is a custom type derived from System.Web.Services.Protocols.SoapHeader that represents a SOAP header. The SoapHeader attribute affixed to the Add method tells ASP.NET to reject calls (that is, throw SoapHeaderExceptions) to Add that lack AuthHeaders and to map incoming AuthHeaders to the SafeService field named header. The Add method extracts the user name and password from the SOAP header and throws an exception if the credentials are invalid.

How does a client attach AuthHeaders to outgoing calls? Here’s an excerpt from a .NET Framework client that calls SafeService’s Add method with the proper authorization header:

SafeService calc = new SafeService ();
AuthHeader header = new AuthHeader ();
header.UserName = "jeffpro";
header.Password = "imbatman";
calc.AuthHeaderValue = header;
int sum = calc.Add (2, 2);

The AuthHeader data type is defined in the service’s WSDL contract. When Wsdl.exe generated the Web service proxy (SafeService), it also generated an AuthHeader class definition and added an AuthHeader field named AuthHeaderValue to SafeService. Initializing that field with an AuthHeader object transmits the data contained therein in a SOAP header. Here, in fact, is what the outgoing SOAP envelope might look like, with the SOAP header highlighted in boldface text:

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd=http://www.w3.org/2001/XMLSchema
  xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <AuthHeader xmlns="http://tempuri.org/">
      <UserName>jeffpro</UserName>
      <Password>imbatman</Password>
    </AuthHeader>
  </soap:Header>
  <soap:Body>
    <Add xmlns="http://tempuri.org/">
      <a>2</a>
      <b>2</b>
    </Add>
  </soap:Body>
</soap:Envelope>

As you can see, the .NET Framework goes to great lengths to insulate you from the nuts and bolts of SOAP and XML. It even provides high-level abstractions for SOAP headers.

A variation on this technique is to include an Application_Authenticate- Request handler in a Global.asax file and examine SOAP headers there. The advantage to this approach is that you don’t have to authenticate the caller in each and every Web method. Application_AuthenticateRequest centralizes the authentication code and allows you to prevent the Web method from even being called if the caller lacks the proper credentials.

Yet another way to identify callers is to configure IIS to require authenticated access to the directory that hosts the Web service and enable Windows authentication in ASP.NET via a Web.config file. Then derive your Web service class from System.Web.Services.WebService and use the inherited User property to obtain callers’ user names. This form of authentication, unlike methods that perform custom authentication using SOAP headers, is appropriate only if callers have accounts in the Web server’s domain. In other words, it’s a fine solution for Web services that serve clients on the company’s intranet but not for ones that serve the population at large.

Dawn of a New Era

Web services have the potential to change the way we compute. Imagine an Internet featuring millions of TerraServices, each exposing different content and business logic to interested callers. The applications that you write would enjoy access to a rich assortment of data and services unparalleled in the industry today. And writing those applications would require little or no knowledge of the underlying communications protocols, thanks to the .NET Framework, which hides HTTP, XML, and SOAP under the hood and allows you to focus on the logic that makes your application unique rather than on the plumbing that enables it to communicate with remote servers.

True cross-platform distributed computing has been the Holy Grail of computer scientists for years, but only now, with the advent of Web services and tools that simplify them, is the dream finally realizable. Anyone who has spent the last several years wrestling with RPC, DCOM, CORBA, and other remote invocation protocols will agree: Web services are an idea whose time has come. For developers, the time to learn about them is now.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.24.106