.NET Support for Metadata and Web Services

Microsoft designed its .NET Developer Studio to be friendly to Web Service authors and consumers. It offers a rich set of tools, wizards, and utility classes to automate the discovery and execution of Web Services.

The .NET Developer Studio enables service developers to quickly build Web Services that can be deployed on either local or remote Web servers. The .NET Developer Studio automates the creation of all the discovery and WSDL contracts. It even creates simple Web documents that help you test and debug your code. All you have to do is write the business logic! The studio also includes a rich API that allows programmers to make high-level calls to remote services. The utilities handle all of the conversion work to and from SOAP messages.

Microsoft is not the only company touting the future of Web Services. Sun is actively working on integrating the idea in their Java 2 Enterprise Edition. This means that Microsoft and Sun Web Services will one day be able to work together, side-by-side!

Think back to our friend John Smith. His company's HR Director has begun a project to reimplement their personnel management system using Web Services. The plan is to use the .NET Developer Studio to create both the personnel management services and clients that access them.

The project team begins by implementing a service that will create new employees in their back-office database. The Web Service will expose a simple createPerson( ) Web method that accepts essential employee information, creates an entry for the person in the employee database, and returns a SOAP document with all the person's properties filled out. In this section we will look more closely at the createPerson( ) Web Service.

The createPerson( ) Web Service

The createPerson( ) Web Service will be implemented in the C# programming language. The ASP .NET Web Service Project wizard initializes most of the infrastructure necessary to create a Web Service. Among other items, it creates the .vsdisco and WSDL files (the WSDL contract is actually generated dynamically by a call to the .asmx service file) that hold all the metadata clients will need to locate and access the service. The .vsdisco file contains links to related WSDL contracts. The wizard also creates a template .asmx file that is the starting point from where coding the service's business logic begins.

Before they can begin coding the service the team needs to add a few classes to the project. The first class defines a Person object and the second defines an Address object. The code behind the Person class is similar to the class definition shown earlier in the chapter, with the exception of address information. Listing 8-8 shows the file.

Listing 8-8 person.cs: A C# source file that contains the Person class definition.

 using System; namespace CSharpWebService { /// <summary> /// A Simple Person Class Definition /// </summary> public class Person { public String firstName; public String lastName; public String birthDate; public String hairColor; public String favoriteColor; public Address address; public Person() { firstName = ""; lastName = ""; birthDate = ""; hairColor = ""; favoriteColor = ""; address = new Address(); } public Person(String _firstName, String _lastName, String _birthDate, 
String _hairColor, String _favoriteColor) { firstName = _firstName; lastName = _lastName; birthDate = _birthDate; hairColor = _hairColor; favoriteColor = _favoriteColor; address = new Address(); } public void setFirstName(String _firstName){ firstName = _firstName; } public void setLastName(String _lastName){ lastName = _lastName; } public void setBirthDate(String _birthDate) { birthDate = _birthDate; } public void setHairColor(String _hairColor) { hairColor = _hairColor; } public void setFavoriteColor(String _favoriteColor) { favoriteColor = _favoriteColor; } public void setAddress(String address1, String address2, String city, String state, String postalCode, String country) { address.setAddress1(address1); address.setAddress2(address2); address.setCity(city); address.setState(state); address.setPostalCode(postalCode); address.setCountry(country); } } }

The properties listed in the Listing 8-8 class definition are basic personnel information: the employee's name, hair color, date of birth, favorite color, and home address. The properties need to be marked public so they can be returned via a SOAP document. The class's methods are simple accessors that manipulate the classes properties. The setAddress( ) method sets the properties on the address dependent object.

The designers of the system decided to create a helper Address class to provide common API for storing addresses. This class might be extended later to support driving directions or other features. The class, as it exists now, is straightforward. It contains a handful of relevant properties and the accessor methods required to manipulate them. The class definition for the address class is contained in Listing 8-9.

Listing 8-9 address.cs: A C# source file containing the Address class definition.

 using System; namespace CSharpWebService { /// <summary> /// A simple address object /// </summary> public class Address { public String address1; public String address2; public String address3; public String city; public String state; public String postalCode; public String country; public Address() { address1 = ""; address2 = ""; address3 = ""; city = ""; state = ""; postalCode = ""; country = ""; } public String getAddress1() { return address1; } public String getAddress2() { return address2; } public String getAddress3() { return address3; } public String getCity() { return city; } public String getState() { return state; } public String getPostalCode() { return postalCode; } public String getCountry() { return country; } public void setAddress1(String _address1){ address1 = _address1;} public void setAddress2(String _address2){ address2 = _address2;} public void setAddress3(String _address3){ address3 = _address3;} public void setCity(String _city){city = _city;} public void setState(String _state){state = _state;} public void setPostalCode(String _postalCode){postalCode = _postalCode;} public void setCountry(String _country){country = _country;} } } 

Now that the underlying objects have been built the development team can begin developing the Web Service logic. Most of the code in this file was generated by the .NET Web Service Wizard. The only code the programmers need to worry about comes after the [WebMethod] C# declaration. Listing 8-10 implements our Web Service.

Listing 8-10 Service1.asmx.cs

 using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Web; using System.Web.Services; namespace CSharpWebService { /// <summary> /// Summary description for Service1. /// </summary> public class Service1 : System.Web.Services.WebService { public Service1() { //CODEGEN: This call is required by the ASP.NET Web Services Designer InitializeComponent(); } #region Component Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { } #endregion /// <summary> /// Clean up any resources being used. /// </summary> protected override void Dispose( bool disposing ) { } [WebMethod] public Person CreatePerson(String firstName, String lastName,  String birthDate, String hairColor, String favoriteColor, String address1, 
String address2, String city, String state, String postalCode, String country) { Person p = new Person(firstName,lastName, birthDate, hairColor, favoriteColor); p.setHomeAddress(address1,address2,city,state,postalCode,country); // Database logic goes here (this code has been omitted for brevity) return p; } } }

Notice that the code following the [WebMethod] declaration is nothing special. The CreatePerson( ) function looks normal in every respect. It doesn't need any special code to retrieve its arguments from a SOAP or HTTP document. It can create objects, load COM components, and access databases just like any other program. Finally the function is free to return a complex object as its result without worrying about creating a SOAP response on the way out. The .NET Framework handles all this hassle for the developer, so much so that the Web Service has no idea that it is being accessed remotely!

Once the service is compiled and deployed (.NET also automates the deployment process), it's ready to be accessed by any type of remote client. Before we write the client, let's first take a step back and look at the metadata the service has to offer. As mentioned earlier, one of the files the wizard created was the Listing 8-7 .vsdisco discovery document. The wizard also placed a reference to this file in the Listing 8-11 discovery file in the root directory of the Web server. These files are listed here.

Listing 8-11 Default.vsdisco: The global discovery file for the Web server that provides links to service-specific discovery documents.

 <?xml version="1.0" encoding="utf-8"?> <discovery xmlns="http://schemas.xmlsoap.org/disco/"> <discoveryRef ref="http://localhost/CSharpWebService/ CSharpWebService.vsdisco" /> <discoveryRef ref="http://localhost/HelloWorld/ HelloWorld.vsdisco" /> </discovery> 

Notice that one of the discovery references in this file points to the URL http://localhost/CSharpWebService/CSharpWebService.vsdicso. Listing 8-12 contains the discovery information for the service we just wrote. Here are the contents of this file.

Listing 8-12 SharpWebService.vsdisco: The createPerson service discovery file.

 <?xml version="1.0" encoding="utf-8"?> <discovery xmlns="http://schemas.xmlsoap.org/disco/"> <contractRef ref="http://localhost/CSharpWebService/Service1.asmx?wsdl" 
docRef="http://localhost/CSharpWebService/Service1.asmx" xmlns=
"http://schemas.xmlsoap.org/disco/scl/" /> </discovery>

Here we are finally redirected to the services WSDL contract. The ref attribute of the <contactRef> element contains a URL to this resource ("http://localhost/CSharpWebService/Service1.asmx?wsdl"). As mentioned earlier, the WSDL contract holds the keys to accessing the Web Service. The metadata contained in this WSDL document is a more complex than the one shown in our HelloWorld example earlier. The complete WSDL document can be found in Listing 8-13.

Listing 8-13 createPerson.wsdl: The createPerson WSDL contract.

 <?xml version="1.0" encoding="utf-8"?> <definitions xmlns:s="http://www.w3.org/2001/XMLSchema" 
xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" xmlns:mime=
"http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tm=
"http://microsoft.com/wsdl/mime/textMatching/" xmlns:soap= "http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soapenc= "http://schemas.xmlsoap.org/soap/encoding/" xmlns:s0= "http://tempuri.org/" targetNamespace= "http://tempuri.org/" xmlns= "http://schemas.xmlsoap.org/wsdl/"> <types> <s:schema attributeFormDefault="qualified" elementFormDefault="qualified" targetNamespace="http://tempuri.org/"> <s:element name="CreatePerson"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name= "firstName" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "lastName" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "birthDate" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "hairColor" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "favoriteColor" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "address1" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "address2" 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:element minOccurs="1" maxOccurs="1" name= "postalCode" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name= "country" nillable="true" type="s:string" /> </s:sequence> </s:complexType> </s:element> <s:element name="CreatePersonResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name= "CreatePersonResult" nillable="true" type="s0:Person" /> </s:sequence> </s:complexType> </s:element>

The primary difference between this WSDL document and the simpler HelloWorld version is that the return type of the createPerson( ) service is a complex object rather than a string. An <s:element> tag is created for each property in the class. The first five properties are all of type string and require no further definition. The homeAddress property, however, is an Address object. The contract must also include a <complexType> definition for Address if a client wants to reference it. The Address class is defined next. Because none of its properties are complex objects, the WSDL document is finished describing classes.

 <s:complexType name="Person"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="firstName"  nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="lastName"  nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="birthDate"  nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="hairColor"  nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="favoriteColor" 
nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="homeAddress"
nillable="true" type="s0:Address" /> </s:sequence> </s:complexType> <s:complexType name="Address"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="address1" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="address2" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="address3" 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:element minOccurs="1" maxOccurs="1" name="postalCode"
nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="country"
nillable="true" type="s:string" /> </s:sequence> </s:complexType> <s:element name="Person" nillable="true" type="s0:Person" /> </s:schema> </types>

The SOAP, GET, and POST request and response documents are similar in nature to those defined in the HelloWorld example. Notice that the CreatePersonXXXOut all return some form of the s0:Person complexElement type declared previously.

 <message name="CreatePersonSoapIn"> <part name="parameters" element="s0:CreatePerson" /> </message> <message name="CreatePersonSoapOut"> <part name="parameters" element="s0:CreatePersonResponse" /> </message> <message name="CreatePersonHttpGetIn"> <part name="firstName" type="s:string" /> <part name="lastName" type="s:string" /> <part name="birthDate" type="s:string" /> <part name="hairColor" type="s:string" /> <part name="favoriteColor" type="s:string" /> <part name="address1" type="s:string" /> <part name="address2" type="s:string" /> <part name="city" type="s:string" /> <part name="state" type="s:string" /> <part name="postalCode" type="s:string" /> <part name="country" type="s:string" /> </message> <message name="CreatePersonHttpGetOut"> <part name="Body" element="s0:Person" /> </message> <message name="CreatePersonHttpPostIn"> <part name="firstName" type="s:string" /> <part name="lastName" type="s:string" /> <part name="birthDate" type="s:string" /> <part name="hairColor" type="s:string" /> <part name="favoriteColor" type="s:string" /> <part name="address1" type="s:string" /> <part name="address2" type="s:string" /> <part name="city" type="s:string" /> <part name="state" type="s:string" /> <part name="postalCode" type="s:string" /> <part name="country" type="s:string" /> </message> <message name="CreatePersonHttpPostOut"> <part name="Body" element="s0:Person" /> </message> <portType name="Service1Soap"> <operation name="CreatePerson"> <input message="s0:CreatePersonSoapIn" /> <output message="s0:CreatePersonSoapOut" /> </operation> </portType> <portType name="Service1HttpGet"> <operation name="CreatePerson"> <input message="s0:CreatePersonHttpGetIn" /> <output message="s0:CreatePersonHttpGetOut" /> </operation> </portType> <portType name="Service1HttpPost"> <operation name="CreatePerson"> <input message="s0:CreatePersonHttpPostIn" /> <output message="s0:CreatePersonHttpPostOut" /> </operation> </portType> <binding name="Service1Soap" type="s0:Service1Soap"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operation name="CreatePerson"> <soap:operation soapAction="http://tempuri.org/CreatePerson" style="document" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <binding name="Service1HttpGet" type="s0:Service1HttpGet"> <http:binding verb="GET" /> <operation name="CreatePerson"> <http:operation location="/CreatePerson" /> <input> <http:urlEncoded /> </input> <output> <mime:mimeXml part="Body" /> </output> </operation> </binding> <binding name="Service1HttpPost" type="s0:Service1HttpPost"> <http:binding verb="POST" /> <operation name="CreatePerson"> <http:operation location="/CreatePerson" /> <input> <mime:content type="application/x-www-form-urlencoded" /> </input> <output> <mime:mimeXml part="Body" /> </output> </operation> </binding> <service name="Service1"> <port name="Service1Soap" binding="s0:Service1Soap"> <soap:address location="http://localhost/CSharpWebService/Service1.asmx" /> </port> <port name="Service1HttpGet" binding="s0:Service1HttpGet"> <http:address location="http://localhost/CSharpWebService/Service1.asmx" /> </port> <port name="Service1HttpPost" binding="s0:Service1HttpPost"> <http:address location="http://localhost/CSharpWebService/Service1.asmx" /> </port> </service> </definitions> 

The createPerson( ) Web Service Client

We will begin writing our client by examining the metadata that our new service will accept and return. We will stick to examples using the SOAP invocation protocol. To reconstruct John Smith's record in the new personnel system we will need to send a SOAP document with the following format to the createPerson( ) method.

 POST /CSharpWebService/Service1.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: length SOAPAction: "http://tempuri.org/CreatePerson" <?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> <CreatePerson xmlns="http://tempuri.org/"> <firstName>John</firstName> <lastName>Smith</lastName> <birthDate>11/30/1974</birthDate> <hairColor>brown</hairColor> <favoriteColor>blue</favoriteColor> <address1>200 Brattle Street</address1> <address2></address2> <city>Cambridge</city> <state>MA</state> <postalCode>02138</postalCode> <country>USA</country> </CreatePerson> </soap:Body> </soap:Envelope>

We could create this document by hand and ship it off to the Web server, but why go through the effort when .NET includes utilities to help do it for us? One such utility is the SoapHttpClientProtocol class. This class provides a layer of abstraction above the underlying SOAP protocol. Developers can use this API to call remote Web Services as if they were local methods.

This class can be used only through sub-classes. You must create your own class that extends SoapHttpClientProtocol and adapt it for a specific Web Service. To implement a sub-class of SoapHttpClientProtocol you must perform at least four tasks.

  1. Provide a default constructor for the class that sets the Url property of the superclass to the location of the Web Service (as defined in the service's WSDL contract).
  2. Create a method in the class that has the same signature as the method in the Web Service.
  3. Call the Invoke method of the SoapHttpClientProtocol class, passing in the service name and an array of objects representing the service arguments.
  4. Provide local class definitions for any complex objects that are returned by the service.

The following code listing shows an example of a SoapHttpClientProtocol sub-class engineered to communicate with the CreatePerson service we created earlier. The file that contains this class definition is Listing 8-14.

Listing 8-14 PersonCreator.cs: A utility class that provides high-level access to the CreatePerson service.

 using System.Diagnostics; using System.Xml.Serialization; using System; using System.Web.Services.Protocols; using System.Web.Services; 

The WebServiceBindingAttribute attribute states that the PersonCreator class contains one or more Web methods that implement the Service1Soap binding, as defined by the WSDL contract for the Web Service. Bindings are similar to interfaces in that they prescribe a strict set of operations. Classes that implement one or more bindings for a Web Service need to make sure that they implement all operations defined within that binding. Let's review the Service1Soap binding that we saw at the beginning of this section.

 <binding name="Service1Soap" type="s0:Service1Soap"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operation name="CreatePerson"> <soap:operation soapAction="http://tempuri.org/CreatePerson" style="document" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> 

The binding implies that the class will contain a CreatePerson( ) Web Service method that implements some sort of SOAP action. The corresponding C# Web method, shown here, uses a C# attribute to identify itself.

 [System.Web.Services.WebServiceBindingAttribute(Name="Service1Soap", 
Namespace="http://tempuri.org/")] public class PersonCreator : System.Web.Services.Protocols. SoapHttpClientProtocol { [System.Diagnostics.DebuggerStepThroughAttribute()] public PersonCreator() { this.Url = "http://localhost/CSharpWebService/Service1.asmx"; } [System.Diagnostics.DebuggerStepThroughAttribute()]

A call to a Web Service method, or operation, can be encoded in two ways: RPC and Document. The RPC style encodes the Web Service method using the SOAP for RPC specification. The Document style, which is used here as indicated by the SoapDocumentMethodAttribute attribute, creates a SOAP XML message that is formatted exactly as defined by the XSD schema contained in the service's WSDL document.

The method signature to CreatePerson( ) also follows the directions of the WSDL document in its arguments and return type.

 [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://tempuri.org/CreatePerson", 
Use=System.Web.Services.Description. SoapBindingUse.Literal, ParameterStyle=System.Web.Services. Protocols.SoapParameterStyle.Wrapped)] public Person CreatePerson(string firstName, string lastName, string birthDate, string hairColor,
string favoriteColor, string address1, string address2, string city, string state, string postalCode,
string country) {

The magic begins with a call to the SoapHttpClientProtocol class's Invoke( ) method. The .NET infrastructure marshals the argument object (an array of String objects) into an XML SOAP document, sends the document to the Web Service, waits for the service to reply, and unmarshals the return result into position zero of the result array. This result is cast to a Person object and is returned to the client.

 object[] results = this.Invoke("CreatePerson", new object[] { firstName, lastName, birthDate, hairColor, favoriteColor, address1, address2, city, state, postalCode, country}); return ((Person)(results[0])); } } 

Local definitions for the Person and Address classes are needed to cast the return result without error. Once again, the WSDL contract has all the information you need to create these class definitions. In fact, these Person and Address class definitions don't necessarily need to match those found in the Web Service. Only the publicly accessible properties (that is, those properties marked as public or accessible through a getter/setter design pattern) need to appear in the client's code.

 public class Person  { public string firstName; public string lastName; public string birthDate; public string hairColor; public string favoriteColor;   public Address homeAddress; } public class Address  { public string address1; public string address2; public string address3; public string city; public string state; public string postalCode; public string country; } 

The service proxy that we just built provides an additional layer of abstraction for our client. We now have a business-level interface to the PersonCreator Web Service. Client programs that use the PersonCreator class do not need to worry about where the Web Service is located or what protocol is used to invoke it. As you can see here, the PersonCreator class is easy to use. You'd never know there was so much complexity—and metadata—underneath the covers.

 PersonCreator personCreator = new PersonCreator(); Person p = personCreator.CreatePerson("John", "Smith", "11/30/1974", 
"brown", "blue", "200 Brattle Street", "", "Cambridge", "MA", "02138", "USA");

This is a slick programming interface. Microsoft's SoapHttpClientProtocol class saved us some time by masking the gory details of SOAP messaging. Still, the process of extending this class every time we want to use a Web Service can become tedious. If this were the only way to work, each time you wanted to use a Web Service you'd have to download its WSDL contract, interpret it (which can be tricky, especially when multiple classes are involved!), and manually create a stub class that implements all of the defined operations. Fortunately, the .NET provides tools to do all this for you.



XML Programming
XML Programming Bible
ISBN: 0764538292
EAN: 2147483647
Year: 2002
Pages: 134

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net