13.            Chapter 13 A Web Service Walkthrough

"The only source of knowledge is experience" – Albert Einstein

 

To wrap up the book and summarize much of what was covered, this chapter walks you through the process of building a live Web service. You will learn how to design Web service messages, how to form those messages with .NET attributes, and how to apply the authentication and authorization infrastructure to a new Web service. You’ll get the most benefit out of this chapter by building the Web service and following along as I explain each step.

Introduction and Requirements

Consider the Weather service I’ve used throughout this book. Now there’s a need to build a new version of this service with some changes and additional features. This section explains the core requirements for this new version.

The new version is based on messaging rather than RPC so the SOAP message format will be different. Specifically, the new service will use document/literal messages rather than RPC/encoded and will ignore SOAPAction HTTP headers.

Like the old version, the new version will expose two primary operations: GetTemperature and GetWeather. However, not everyone will have access to both operations. Instead, the service will be secured so that only registered users can invoke its operations (they must logon to the service first). In addition, each registered user will have a profile that lists which operations they can access. For example, a user might have access to GetTemperature but not GetWeather.

Since users are now registered and each user must logon to invoke an operation, I would like to keep track of the Web service usage by each user. This concept of usage accounting is common for public Web services and enables us to bill for Web service usage in the future (not that I intend to charge people for using the Weather service, but you might build a service and charge for using it).

Finally, the service should be registered with UDDI so that clients may implement the UDDI Invocation pattern (see Chapter 11). When registering with UDDI, the service’s interface will be registered as a new tModel. This tModel must point to a WSDL document that contains interface-only elements, i.e. no <service> element.

 

Designing Web Service Messages

The first step in implementing a document/literal Web service is to design the messages exchanged between client and service. To do this you create an XSD schema describing each message. You can use a specialized XSD editor or any text editor including notepad. I used VS .NET’s Schema editor because it provides an easy GUI for building the base schema structure and gives you access to the schema’s text in case you want to do some manual editing. To use VS .NET’s Schema editor, go to the File menu and choose New, File. On the New File dialog select XML Schema as shown in figure 13-1.


Figure 13‑1

Creating a new schema

This will give you the Schema designer with its toolbox and properties window as shown in figure 13-2. The properties window is useful for changing things like the schema’s targetNamespace, elementFormDefault, and attributeFormDefault settings (see chapter 2 for more information on these settings). I changed the schema targetNamespace to http://www.learnxmlws.com/WeatherService and set the id property to WeatherMessages.

The toolbox contains the building blocks you use when creating a schema. Things like element and attribute declarations, complex and simple type definitions, facets, and keys (see chapter 2 for more information on XML schema).


Figure 13‑2

The Visual Studio Schema Designer

 

To create a schema, you drag objects from the toolbox to the designer surface and set their properties. For example, to add an element called Temperature you drag an element option from the toolbox to the designer surface. By default, the element will be called element1 and its type will be (element1). You can then change the element name to Temperature and its type to float. Here’s the resulting element box as you’ll see it on the designer surface:


Figure 13‑3

A simple element in the schema designer


To create an element that has an anonymous complex type, you simply start adding child elements and attributes inside the element box. For example, if you create an element called CurrentWeather then add to it a Temperature element of type float and a Conditions element of type string here’s what you get in the designer:

Figure 13‑4

An element with an anonymos complex type

Note: To change a child element to an attribute click on the E next to the element and select attribute from the dropdown list as shown in figure 13-5.

Figure 13‑5

Changing a child element to an attribute

 

Figure 13-6 shows the designer view for the request and response messages and listing 13-1 shows the schema (text) view.

 

Figure 13‑6

Using The VS .NET Schema designer to design Web service messages.

 

Listing 13‑1 A schema defining Web service messages. (VBWSBook\Chapter13\WeatherMessages.xsd).

<xs:schema id="WeatherMessages"

targetNamespace="http://www.learnxmlws.com/WeatherService"

elementFormDefault="qualified"

xmlns="http://www.learnxmlws.com/WeatherService"

xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <!-- the GetTemperature request with the zipcode -->

    <xs:element name="TemperatureRequest" type="xs:string"/>

    <!-- response with temperature -->

    <xs:element name="Temperature" type="xs:float"/>

    <!-- GetWeather request with zipcode -->

    <xs:element name="WeatherRequest" type="xs:string"/>

    <!-- GetWeather response with CurrentWeather -->

    <xs:element name="CurrentWeather">

        <xs:complexType>

            <xs:sequence>

                <xs:element name="Temperature" type="xs:float" />

                <xs:element name="Conditions" type="xs:string" />

                <xs:element name="IconUrl" type="xs:string" />

                <xs:element name="Humidity" type="xs:float" />

                <xs:element name="Barometer" type="xs:float" />

            </xs:sequence>

        </xs:complexType>

    </xs:element>

</xs:schema>

I wanted to keep request and response messages as simple as possible so I tried not to use complex types unless they were really needed. For example, both request messages, TemperatureRequest and WeatherRequest are simply elements that contain the requested ZipCode directly so they are of type string. This design reduces a request message from the current form in the old weather service (namespaces omitted):

 

<GetTemperature>

      <zipCode>20171</zipCode>

</GetTemperature>

 

To this simpler form in the new service (namespaces omitted):

 

<TemperatureRequest>20171</TemperatureRequest>

Similarly, the response from GetTemperature is equally simple: An element named Temperature that contains the returned temperature value. However, the response from GetWeather has to be a complex type to carry the weather information. A typical GetWeather response message would look like this:

 

<CurrentWeather xmlns="http://www.learnxmlws.com/WeatherService">

  <Temperature>float</Temperature>

  <Conditions>string</Conditions>

  <IconUrl>string</IconUrl>

  <Humidity>float</Humidity>

  <Barometer>float</Barometer>

</CurrentWeather>

 

Note that all these messages do not contain the traditional wrapper elements created by .NET. For example, a straightforward GetTemperature implementation would result in the following request and response messages:

<!-- traditional GetTemperature request message -->

<GetTemperature xmlns="http://www.learnxmlws.com/WeatherService">

   <TemperatureRequest>string</TemperatureRequest>

</GetTemperature>

 

<!-- traditional GetTemperature response message -->

<GetTemperatureResponse xmlns="http://www.learnxmlws.com/WeatherService">

   <Temperature>float</Temperature>

</GetTemperatureResponse>

 

Clearly this is not what we want. Not only does it add unnecessary wrapper elements, it also creates the impression that this Web service is based on RPC-style design when in fact I started by designing the messages not the RPC interface. Let’s take a look at how you can implement a .NET Web service that receives and emits the messages described in listing 13-1.

Implementing the Service

There are two options for implementing the messages we designed. First, you could create a WSDL document using the messages schemas from listing 13-1 then run wsdl.exe –server on this WSDL to generate a Web service stub implementation. If you do this, you are essentially defining the interface first (the WSDL), then implementing it which is exactly what I explained in chapter 8.

Alternatively, you can code the Web service directly and use a combination of attributes on the service itself as well as on each Web method to ensure the emitted WSDL adheres to the message specification in listing 13-1. Chapter 8 already covered the interface-based approach so I will take the second approach here. But if you are comfortable enough with WSDL, I recommend you try the first approach at least once for this Web service just to get a feel for it.

Generating Classes from Complex Types

First thing we’ll do is generate classes that correspond to complex types in service messages. In this example, only the CurrentWeather element has a complex type. To generate a class that corresponds to this type, you run xsd.exe with the /classes option. Since you want to generate a class for one of the elements in the schema (rather than classes for all elements in the schema), you specify the element name and namespace with the /e and /u options respectively.

 

xsd.exe /classes /e:CurrentWeather /u:http://www.learnxmlws.com/WeatherService  /l:vb WeatherMessages.xsd

 

This generates a class named CurrentWeather in a file named WeatherMessages.vb. I renamed this file to Messages.vb and removed from it the namespace prefix System.Xml.Serialization because this namespace is already imported at the top of the file. The resulting class, in listing 13-2, contains a public field for each element defined in the complex type. We will use this class to return weather information from the GetWeather operation.

Listing 13‑2 A class generated by xsd.exe from the CurrentWeather element schema. (VBWSBook\Chapter13\Messages.vb).

<XmlTypeAttribute( _

  [Namespace]:="http://www.learnxmlws.com/WeatherService"), _

XmlRootAttribute( _

  [Namespace]:="http://www.learnxmlws.com/WeatherService", _

  IsNullable:=False)> _

Public Class CurrentWeather

    Public Temperature As Single

    Public Conditions As String

    Public IconUrl As String

    Public Humidity As Single

    Public Barometer As Single

End Class

Writing the Service Code

Now you actually get to write some code. Start by creating a Web service project then add to it the CurrentWeather class that was previously generated. Then add a Web service named Weather. Open the code view for this Web service and add Import statements for System.Web.Services.Protocols and System.Xml.Serialization namespaces.

This service should ignore SOAPAction as specified in the requirements section, therefore add to the Web service class a SoapDocumentService attribute and set its RoutingStyle property to SoapServiceRoutingStyle.RequestElement.

By default, there should be a WebService attribute on the Web service class. Modify the namespace property of this attribute to http://www.learnxmlws.com/WeatherService. In this particular example, I chose to add a WebServiceBindingAttribute and specify the binding’s name and namespace as shown in listing 13-3. This is because the Web service’s binding will be registered with UDDI as a tModel and I wanted it to have a more descriptive name than the standard “WeatherSoap”.

To implement the GetTemperature operation, simply create a function named GetTemperature that takes in a Sring parameter and returns a Single as in listing 13-3.

 

Listing 13‑3 A Web Service implementing the pre-defined messages. (VBWSBook\Chapter13\Weather.asmx.vb).

<SoapDocumentService( _

    RoutingStyle:=SoapServiceRoutingStyle.RequestElement), _

WebService(Namespace:="http://www.learnxmlws.com/WeatherService"), _

WebServiceBindingAttribute("WeatherInterface", _

     "http://www.learnxmlws.com/WeatherService")> _

Public Class Weather

    Inherits System.Web.Services.WebService

 

    'Note that we don't want to use SoapAction

    'so .NET relies on request element name

    'so if we use Bare we must ensure there's only one parameter

    'And that parameter name must be unique

    <WebMethod(), _

    SoapDocumentMethod( _

        Binding:="WeatherInterface", _

        Action:="", _

        ParameterStyle:=SoapParameterStyle.Bare)> _

    Public Function GetTemperature( _

        <XmlElement("TemperatureRequest")> ByVal ZipCode As String) _

        As <XmlElement("Temperature")> Single

        Return WeatherInfo.GetTemperatureFromZip(ZipCode)

    End Function

 

    <WebMethod(), _

    SoapDocumentMethod( _

        Binding:="WeatherInterface", _

        Action:="", _

        ParameterStyle:=SoapParameterStyle.Bare)> _

    Public Function GetWeather( _

       <XmlElement("WeatherRequest")> ByVal ZipCode As String) _

          As <XmlElement("CurrentWeather")> CurrentWeather

        Return WeatherInfo.GetWeatherFromZip(ZipCode)

    End Function

End Class

In addition to the WebMethod attribute, this function also has a SoapDocumentMethod attribute which is used to control various aspects of the operation. The binding property specifies that this operation is part of the WeatherInterface binding defined earlier. The Action property specifies that the SOAPAction should be an empty string. I expected that setting the service’s RoutingStyle to RequestElement meant the Web service would not require a SOAPAction. But it turns out that SOAPAction is still required, but its value is ignored. The only reason I set it to an empty string here is to make it clear to you that the value of SOAPAction does not matter.

Finally, the ParameterStyle property is set to SoapParameterStyle.Bare meaning the ZipCode parameter and. the return value will not be wrapped in an additional XML element. This setting works with the XmlElement serialization attributes on the ZipCode parameter and the return value to form the correct request and response message according to the schema in listing 13-1. As a result, the request element contains only the input parameter as a direct child of the SOAP <Body> element. In this case, we want the request message to contain one element named TemperatureRequest, therefore I added an XmlElement attribute on the ZipCode parameter to set the element name to TemperatureRequest. Similarly, the return value will be represented as an element contained directly within the response message’s <Body> element. We want this element to be called Temperature per the schema in listing 13-1, so I used another XmlElement attribute on the return value to specify Temperature as the element name.

The GetWeather Web method has the same SoapDocumentMethod attribute. This time, the ZipCode parameter is serialized to an element named WeatherRequest and the returned CurrentWeather object is serialized to an element named CurrentWeather.

Getting Weather Information

The National Weather Service (NWS) exposes current weather information via FTP and HTTP (at weather.noaa.gov). To make it easy for you to retrieve Weather information, I created a class named WeatherInfo that uses HTTP to get the weather from NWS’s Web site.

To get weather information for a specific location, you must know the weather station id corresponding to that location. Realistically, most people don’t know their weather station id, therefore this service needs a way to translate a U.S. postal zip code to a weather station id. I did some digging and data mining and created a database table that provides this mapping.

Although it has close to 35,000 records, the table does not contain all current zip codes. This should not be a problem since the main purpose of this exercise is to illustrate the process of building Web services by building a live Web service. So don’t expect that the Web service built in this chapter will use production-class data.

 

Going back to listing 13-3, you’ll notice that both GetTemperature and GetWeather rely on the WeatherInfo class to get current weather information. WeatherInfo exposes two shared (static) methods named GetTemperatureFromZip and GetWeatherFromZip. Internally, both methods retrieve complete weather information but GetTemperature returns only the temperature and ignores the rest of the information. You’ll find the WeatherInfo class in the file named WeatherInfo.vb.

 

LogOn and LogOff

To prepare for implementing security, we’ll need to implement two additional Web methods named LogOn and LogOff as shown in listing 13-4.

Each of LogOn and LogOff uses shared methods of a class named SessionMgr to do the real work (SessionMgr is part of the security infrastructure built in chapter 10. See chapter 10 for more information on the SessionMgr class).

LogOff is an especially interesting method for two reasons. First, it doesn’t return any value; therefore it’s a Sub not a Function. Second, when a client calls LogOff, it will probably won’t be interested in finding out whether this LogOff succeeded or not. Consider a client that is being shutdown, it might fire off a LogOff request as it is shutting down. The last thing the client needs is to wait for a response to come back. To make things easier for clients, I marked this method as one-way by setting the OneWay property to True. This means the corresponding WSDL operation will have an input message but won’t have an output message.

 

Listing 13‑4 Adding LogOn and LogOff methods for authentication. (VBWSBook\Chapter13\Weather.asmx.vb).

<WebMethod(), _

SoapDocumentMethod( _

 Binding:="WeatherInterface", _

 Action:="", _

 RequestElementName:="LogOnRequest", _

 ResponseElementName:="NewSession")> _

Public Function LogOn(ByVal UserId As String, ByVal Password As String) _

    As <XmlElement("SessionId")> String

        'implementation coming in a later section

End Function

   

<WebMethod(), _

SoapDocumentMethod( _

 Binding:="WeatherInterface", _

 OneWay:=True, _

 Action:="", _

 RequestElementName:="LogOffRequest", _

 ParameterStyle:=SoapParameterStyle.Bare)> _

Public Sub LogOff(ByVal SessionId As String)

    'implementation coming in a later section

End Sub

 

The RequestElement-Bare Dilemma

Looking at the LogOn method in listing 13-4, you’ll notice that the ParameterStyle property is missing from the SoapDocumentMethod attribute hence the default (which is SoapParameterStyle.Wrapped) will apply. This means that parameters will be wrapped in a request element and the return value will be wrapped in a response element. The reasons why I had to do this are explained in the next few paragraphs.

When you tell .NET to ignore the SOAPAction header (by setting RoutingStyle to RequestElement), you are essentially relying on the request element’s name to identify which method should be invoked. So each request must have exactly one element as a direct child of <Body> and that element must have a unique name for each Web method on your service.

If you get rid of the wrapper elements by setting ParameterStyle to Bare, you are saying that serialized method parameters should appear as direct children of <Body>. When you combine both settings (RountingStyle=RequestElement and ParameterStyle=Bare) you end up with an interesting dilemma: Each Web method must have exactly one parameter and that parameter must have a unique serialized name among all other Web method parameters. For example, you can’t have two methods, GetTemperature and GetWeather, both with a parameter that gets serialized to an element named ZipCode. To see why this makes sense, consider the following request message:

<soap:Body>

   <ZipCode

     xmlns="http://www.learnxmlws.com/WeatherService">20171</ZipCode>

</soap:Body>

Is this a request for the GetTemperature or GetWeather web method? If both methods have one parameter named ZipCode then there’s no way to tell which method the client intended to invoke. Therefore, each method must have exactly one parameter and the serialized parameter must have a unique name.

The LogOn method has two parameters: User id and password so it can’t use Bare parameter style and RequestElement routing style together. I could replace the user id and password with a structure or a class that has two public members called UserId and Password and make LogOn accept an object of this class. But that would make it a little less convenient for a .NET client as they would have to instantiate an object and set its members then pass it to LogOn as opposed to just calling LogOn and passing it the UserId and password directly.

I decided to keep things simple for .NET clients so I chose to use the default ParameterStyle which is wrapped. This is a design decision and there’s no absolute right or wrong so your choice could very well be different from mine.

 

Authentication and Authorization

At this point, we have the core Web service operations implemented and working. However, there’s still a lot of work to be done. We must secure the Web service so that only registered users with the appropriate permissions can access each of the methods. We must also modify the standard, auto-generated, documentation page to inform users that registration is required and give them a link to the registration page. We also need to implement a usage tracking system so that we can monitor the usage for each user and, possibly, bill for that usage.

As you can see, a typical Web service requires substantial infrastructure. Instead of building security and usage accounting from scratch we will simply use the infrastructure SOAP extensions from chapter 10.

To reuse the infrastructure SOAP extension from chapter 10, you first need to add a reference to infrastructure.dll (the assembly that contains the SOAP extensions). You then add an Import statement for the namespace LearnXmlWS.Web.Services.Infrastructure as shown in listing 13-5.

Listing 13‑5 Adding SoapHeader attribute to GetTemperature and GetWeather as part of  implementing authorization. (VBWSBook\Chapter13\Weather.asmx.vb).

Imports System.Web.Services

Imports System.Web.Services.Protocols

Imports System.Xml.Serialization

Imports LearnXmlWS.Web.Services.Infrastructure

<SoapDocumentService( _

    RoutingStyle:=SoapServiceRoutingStyle.RequestElement), _

WebService(Namespace:="http://www.learnxmlws.com/WeatherService"), _

WebServiceBindingAttribute("WeatherInterface", _

     "http://www.learnxmlws.com/WeatherService")> _

Imports LearnXmlWS.Web.Services.Infrastructure

Public Class Weather

    Inherits System.Web.Services.WebService

 

    'The session id

    Public sessHdr As SessionHeader

<WebMethod(), _

SoapDocumentMethod( _

    Binding:="WeatherInterface", _

    Action:="", _

    ParameterStyle:=SoapParameterStyle.Bare), _

 SoapHeader("sessHdr", _

   Required:=True, Direction:=SoapHeaderDirection.In)> _

Public Function GetTemperature( _

    <XmlElement("TemperatureRequest")> ByVal ZipCode As String) _

    As <XmlElement("Temperature")> Single

    Return WeatherInfo.GetTemperatureFromZip(ZipCode)

End Function

 

<WebMethod(), _

SoapDocumentMethod( _

    Binding:="WeatherInterface", _

    Action:="", _

    ParameterStyle:=SoapParameterStyle.Bare), _

SoapSecurity("Weather"), _

SoapHeader("sessHdr", _

   Required:=True, Direction:=SoapHeaderDirection.In)> _

Public Function GetWeather( _

   <XmlElement("WeatherRequest")> ByVal ZipCode As String) _

      As <XmlElement("CurrentWeather")> CurrentWeather

    Return WeatherInfo.GetWeatherFromZip(ZipCode)

End Function

End Class

Recall that the security SOAP extension relies on a SOAP header of type SessionHeader. So you will need to add a public variable to your Web service class. The variable is sessHdr and its type is SessionHeader as shown in listing 13-5.

Next, you need to add the SoapHeader attribute to both GetTemperature and GetWeather methods and specify that it is a required attribute and its direction is In. To apply authorization, you need to add the SoapSecurity attribute to each method and pass its constructor the required permission to execute that method. You can make up those permissions as long as they match what’s in the UserPermissions database table (see chapter 10 for more information about the database schema used for authentication and authorization).

Usage Accounting

Usage accounting can also be implemented by simply applying the usage accounting SOAP extension built in chapter 10. You do this by adding the Accounting attribute to each method for which you want to track usage accounting. Assuming you already applied the security extension, you don’t need to do anything else to apply usage accounting. Listing 13-6 shows the GetTemperature and GetWeather methods with the Accounting attribute added.

 

Listing 13‑6 Applying the security and usage accounting SOAP extensions using attributes. (VBWSBook\Chapter13\Weather.asmx.vb).

<WebMethod(), _

SoapDocumentMethod( _

    Binding:="WeatherInterface", _

    Action:="", _

    ParameterStyle:=SoapParameterStyle.Bare), _

SoapSecurity("Temperature"), _

SoapHeader("sessHdr", _

   Required:=True, Direction:=SoapHeaderDirection.In), _

Accounting (LogResponse:=True)> _

Public Function GetTemperature( _

    <XmlElement("TemperatureRequest")> ByVal ZipCode As String) _

    As <XmlElement("Temperature")> Single

    Return WeatherInfo.GetTemperatureFromZip(ZipCode)

End Function

 

<WebMethod(), _

SoapDocumentMethod( _

    Binding:="WeatherInterface", _

    Action:="", _

    ParameterStyle:=SoapParameterStyle.Bare), _

SoapSecurity("Weather"), _

SoapHeader("sessHdr", _

   Required:=True, Direction:=SoapHeaderDirection.In), _

Accounting(LogResponse:=True)> _

Public Function GetWeather( _

   <XmlElement("WeatherRequest")> ByVal ZipCode As String) _

      As <XmlElement("CurrentWeather")> CurrentWeather

    Return WeatherInfo.GetWeatherFromZip(ZipCode)

End Function

Customizing the Documentation Page

When a user uses a browser to navigate to the Web service end point (e.g. weather.asmx) he or she will get a standard help page that lists the Web service’s methods. This help page is generated by the file DefaultWsdlHelpGenerator.aspx. We need to customize this page to inform the user that this service requires registration and give him or her a link to the registration page.
The name of the default help page is specified in machine.config in the section called WebServices:

 

<wsdlHelpGenerator href="DefaultWsdlHelpGenerator.aspx"/>

 

To customize the generated documentation pages, make a copy of this .aspx file and do your customization in this copy. Then edit the machine.config file and specify the name of your copy instead of DefaultWsdlHelpGenerator.aspx. Alternatively, if you want the custom page to be used for a specific vroot then add the following to the <system.web> section in the vroot’s web.config file.

 

<webServices>

    <wsdlHelpGenerator href="YourCustomPage.aspx"/>

</webServices>

 

The default page contains a large chunk of server-side script that inspects your Web service class to retrieve information such as the list of Web methods and example SOAP request and response for each method. After this script comes the HTML used to display this information. If you want to customize the page’s look, you’ll want to customize this HTML. For example, I created a page called CustomHelpGenerator.aspx informing users about the registration requirement to use this Web service. The custom help page is shown in figure 13-7. Note that the same header is displayed for the method listing page as well as for each method test page.

 

 

Figure 13‑7

A custom help page notifying users that registration is required. Note that the registration link should point to a user registration Web form.

 

A VB 6 Client

To test the Web service, I decided to build a VB 6 client with the SOAP Toolkit’s low level API. The project includes a form and a class module that acts as the Web service proxy. Listing 13-7 shows the pertinent parts of the class module’s code (the entire class is in the file WeatherClient.cls).

 

Listing 13‑7 Implementing a VB 6 client using the SOAP Toolkit. (VBWSClientCode\Chapter13\VB6Client\WeatherClient.cls).

Option Explicit

Private m_SessionHeader As String

Private m_SessionId As String

Private Const SERVICE_URL = "http://vbwsserver/vbwsbook/Chapter13/Weather.asmx"

'Private Const SERVICE_URL = "http://www.learnxmlws.com/weather/Weather.asmx"

Private m_Connector As SoapConnector30

Private m_UseProxy As Boolean

Public ProxyServer As String

Public ProxyPort As Integer

Private Function SendMessage(ByVal header As String, _

           ByVal body As String, _

           Optional ByVal IsOneWay As Boolean = False) _

             As MSXML2.IXMLDOMElement

   On Error GoTo eh

    If m_Connector Is Nothing Then

        ConnectToService

    End If

    m_Connector.BeginMessage

 

    Dim Serializer As SoapSerializer30

    Set Serializer = New SoapSerializer30

    Serializer.Init m_Connector.InputStream

    'write the SOAP message

    Serializer.StartEnvelope

    If Len(header) > 0 Then

       Serializer.StartHeader

       Serializer.WriteXml header

       Serializer.EndHeader

    End If

    Serializer.StartBody

    'write the request document directly

    Serializer.WriteXml body

    Serializer.EndBody

    Serializer.EndEnvelope

    Serializer.Finished

    'send the message

    m_Connector.EndMessage

    If Not IsOneWay Then

        'get the response

        Dim Rdr As SoapReader30

        Set Rdr = New SoapReader30

        Rdr.Load m_Connector.OutputStream

        If Rdr.Fault Is Nothing Then

            If Rdr.BodyEntries.length > 0 Then

                Set SendMessage = Rdr.BodyEntries.Item(0)

            End If

        Else

            On Error GoTo 0

            Err.Raise vbObjectError + 200, _

                "WeatherClient", _

                "The server returned a Fault: " & Rdr.Fault.Text

        End If

        Set Rdr = Nothing

    End If

   

    Set Serializer = Nothing

   

    Exit Function

eh:

   Err.Raise vbObjectError + 100, "WeatherClient", _

            "Error sending SOAP message: " + Err.Description

End Function

 

Public Sub LogOn(ByVal userid As String, ByVal password As String)

    Dim requestMessage As String

    requestMessage = "<LogOnRequest " & _

       " xmlns='http://www.learnxmlws.com/WeatherService'>" & _

       "<UserId>" & userid & "</UserId>" & _

       "<Password>" & password & _

       "</Password></LogOnRequest>"

                      

      Dim SessionId As IXMLDOMElement

      'get the session id

      Set SessionId = SendMessage("", requestMessage)

      If Not (SessionId Is Nothing) Then

          m_SessionId = RemoveWhiteSpace(SessionId.Text)

          'make the session header for later use

          MakeSessionHeader m_SessionId

      End If

End Sub

 

Public Function GetWeather(ByVal zipcode As String) _

             As MSXML2.IXMLDOMElement

    Dim requestMessage As String

    requestMessage = "<WeatherRequest " & _

           "xmlns='http://www.learnxmlws.com/WeatherService'>" & _

           zipcode & "</WeatherRequest>"

                     

      Dim weather As IXMLDOMElement

      Set GetWeather = SendMessage(m_SessionHeader, requestMessage)

     

End Function

 

Private Sub ConnectToService()

    Set m_Connector = Nothing

    Set m_Connector = New HttpConnector30

    m_Connector.Property("EndPointURL") = SERVICE_URL

    m_Connector.Property("SoapAction") = ""

    If m_UseProxy Then

        m_Connector.Property("ProxyServer") = Me.ProxyServer

        m_Connector.Property("ProxyPort") = Me.ProxyPort

        m_Connector.Property("UseProxy") = True

    End If

    'establish connection

    m_Connector.Connect

End Sub

SendMessage is the core method which is responsible for sending SOAP messages and receiving responses. The code first checks if the member variable m_Connector is nothing. If it is, ConnectToService is called to create a new HttpConnector30, set its properties, and connect to the service. This new connector object is stored in m_Connector for future use.

SendMessage then creates a new SoapSerializer30 object and calls StartEnvelope to begin writing the SOAP envelope. If a header string is passed it, it gets written out to the request message by first calling Serializer.StartHeader then Serializer.WriteXml and passing it the header string. This assumes that the header string is a well-formed XML fragment that represents the header you want to send.

Next, the body is written in a similar way, first call StartBody followed by WriteXml to write the body string. When serialization is finished, the message is sent by calling m_Connector.EndMessage.

Then comes a check for the IsOneWay flag to determine whether a response needs to be retrieved. This is needed because SendMessage does not read the service’s WSDL document so it has no way of knowing whether an operation is one-way unless the caller of SendMessage sets the IsOneWay parameter to True.

If the operation is not one-way, SendMessage creates a new SoapReader30 object and uses it to read the output stream. It then uses Rdr.Fault to check for errors and returns the Fault text if there is an error. If no errors are found, it gets the first body entry and returns it. The assumption here is that the returned XML will be contained within one element that is a direct child of Body.

Listing 13-7 also shows the LogOn and GetWeather methods (LogOff and GetTemperature are implemented but not shown in this listing). LogOn creates the request message using primitive string concatenation. Instead of string concatenation, you can use the XML Document Object Model (DOM)  to form your request messages. You can store template request messages in an XML document on the client and use the DOM API to read a message template from this document and substitute data as needed. The resulting XML would then be the request message.

After forming the request message, LogOn calls SendMessage to send the requestMessage and get back the response in the form on an XML DOM element. It then uses the Text property to extract the sessionid out of this element and calls RemoveWhiteSpace to remove leading and trailing carriage return, line feed, and space characters. It then stores this sessionid in the member variable m_SessionId and calls MakeSessionHeader to prepare the SessionId header for future use with GetWeather and GetTemperature.

When GetWeather is called, it forms the WeatherRequest message with the supplied zip code and sends it along with the SessionId header using SendMessage. It then returns the CurrentWeather information as an XML element.

Listing 13-8 shows the form code that uses this proxy class to retrieve and display weather information.

Listing 13‑8 A VB 6 form to invoke the Weather service. (VBWSClientCode\Chapter13\VB6Client\Form1.frm).

Option Explicit

Private m_Weather As WeatherClient

Private Sub cmdGet_Click()

On Error GoTo eh

    If m_Weather Is Nothing Then

        CreateProxy

    End If

    m_Weather.UseProxy = chkProxy.Value

   

    Dim cw As MSXML2.IXMLDOMElement

    Set cw = m_Weather.GetWeather(txtZip.Text)

    DisplayData cw

    Exit Sub

eh:

    lblInfo.Caption = Err.Description

End Sub

Private Sub CreateProxy()

    Set m_Weather = New WeatherClient

    m_Weather.ProxyServer = "localhost"

    m_Weather.ProxyPort = 8080

    m_Weather.UseProxy = chkProxy.Value

    m_Weather.LogOn "user01", "password"

End Sub

 

Private Sub Form_Unload(Cancel As Integer)

    'intentionally ignore errors

    'this is a one-way method

    'and the form is closing

    On Error Resume Next

    If Not (m_Weather Is Nothing) Then

        m_Weather.LogOff

    End If

End Sub

When the user clicks Get Weather, the cmdGet_Click code first checks if the service proxy, stored in m_Weather, has already been created. If not, CreateProxy is called. CreateProxy instantiates a new WeatherClient object, configures its proxy settings, then calls the LogOn method to obtain a sessionid.


Going back to cmdGet_Click, once the proxy is created, cmdGet_Click calls its GetWeather method passing it the user-entered zip code. It then captures the returned XML element and passes it to DisplayData which uses the DOM to extract and display weather information. The result is shown in figure 13-8.

 

Figure 13‑8

Running the VB 6 client.

 

Leveraging UDDI

In order to provide some recovery facility, I registered the Weather service with production UDDI registry to enable clients to implement the UDDI invocation pattern (see Chapter 11). To register with UDDI, I first saved off the service’s WSDL then edited it and removed the implementation part, i.e. the <service> element. I saved the resulting WSDL document at http://www.learnxmlws.com/weather/weatherinterface.wsdl. I already had a business entity registered with UDDI so all I had to do was register a new tModel and then register the service itself. The resulting service key is C6DDD2FB-593B-452A-A225-C6A930EFCDE8 and the binding key (which is what you use for the UDDI invocation model) is 400d2ea3-bc66-485d-9594-b3474d801442.

 

Implementing the Invocation Pattern

I chose to implement the UDDI invocation pattern in a .NET client in order to show you two different clients for the Weather service. Listing 13-9 shows a partial listing of the client code that retrieves and displays weather information. See frmWeather.vb for the complete code.

Listing 13‑9 A VB .NET client that uses the UDDI invocation pattern to locate the Web service at runtime. (VBWSClientCode\Chapter13\DotNetClient\frmWeather.vb).

Const BINDING_KEY As String = "eb045631-c5bb-4621-b052-830c13b5d7c8"

 

Private _ws As VBWSSERVER.Weather

Private Sub btnWeather_Click( _

 ByVal sender As System.Object, ByVal e As System.EventArgs) _

     Handles btnWeather.Click

    Dim cw As CurrentWeather

    Try

        cw = GetWeather(txtZip.Text)

    Catch ex As Exception

            If UDDIInvocationPattern.IsRecoverable(ex) Then

                lblStatus.Text = "Checking UDDI ..."

                Me.Refresh()

                If GetCurrentUrl() Then

                    cw = GetWeather(txtZip.Text)

                    lblStatus.Text = ""

                End If

            Else

                MessageBox.Show(ex.Message)

                _ws = Nothing

            End If

    End Try

    If Not (cw Is Nothing) Then

        ShowWeatherInfo(cw)

    End If

End Sub

Private Function GetWeather(ByVal ZipCode) As VBWSSERVER.CurrentWeather

   If _ws Is Nothing Then

        CreateProxy()

   ElseIf chkProxy.Checked Then

        _ws.Proxy = New System.Net.WebProxy("http://localhost:8080", False)

   Else

        _ws.Proxy = Nothing

   End If

   Return _ws.GetWeather(ZipCode)

End Function

 

Private Sub CreateProxy()

    _ws = New Weather()

    Me.SetProxy()

    Me.DoLogOn()

End Sub

Private Sub SetProxy()

    If chkProxy.Checked Then

        _ws.Proxy = New _

          System.Net.WebProxy("http://localhost:8080", False)

    End If

End Sub

Private Sub DoLogOn()

    'you can change the userid and password here ...

    Dim sess As New SessionHeader()

    sess.SessionId = _ws.LogOn("user01", "password")

    _ws.SessionHeaderValue = sess

End Sub

Private Function GetCurrentUrl()

        Try

            Dim newUrl As String = _

                    UDDIInvocationPattern.GetCurrentUrl(BINDING_KEY)

            _ws.Url = newUrl

        Catch ex As Exception

            MessageBox.Show( _

               "There was an error retrieving the current URL from UDDI." + _

               " Please try again later. " + _

               ex.Message)

            Return False

        End Try

        Me.DoLogOn()

        Return True

End Function

 

Private Sub frmWeather_Closing(ByVal sender As Object, _

   ByVal e As System.ComponentModel.CancelEventArgs) _

                 Handles MyBase.Closing

    Try

        If Not (_ws Is Nothing) Then

            'call LogOff to end the session

            _ws.LogOff(_ws.SessionHeaderValue.SessionId)

        End If

    Catch ex As Exception

        'ignore the exception

    End Try

End Sub

When the user clicks Get Weather, the event handler calls GetWeather passing it the user-entered zip code. If an exception occurs while getting the weather information, the Catch block checks to see if this is a recoverable error and if so it calls GetCurrentUrl to get the current Web service end point URL from UDDI. It then proceeds to call GetWeather again using this new URL. If the exception is non-recoverable, it is reported in a message box. Once weather information is retrieved, ShowWeatherInfo (not shown here) handles displaying it to the user.

The GetWeather function first checks if the Web service proxy is created. If not, it calls CreateProxy which instantiates a new Web service proxy then calls SetProxy to configure the HTTP proxy then calls DoLogOn to invoke the Web service’s LogOn method. If the Web service proxy was already created, GetWeather sets the HTTP proxy if the user checked the Use Proxy check box. It then calls the Web service’s GetWeather passing it the ZipCode. When the form is closing, it calls the Web service’s LogOff method catching and ignoring any exceptions that might occur.

You can test this client by first pointing it to the weather Web service on your own server. Then stop your server (for example by typing iisreset –stop on the command line) and click Get Weather. The client will query UDDI, get the Web service URL (on LearnXmlWS.com) then go on and invoke the service at this location.


Figure 13‑9

Using the VB .NET client. This client implements the UDDI Invocation Pattern to attempt to recover from failures.

Summary

This chapter was a walkthrough of the Web service development process. The process begins with message design where you create XML Schemas describing the service’s input and output messages. You then use xsd.exe to generate complex types from those schemas as needed. To implement the service itself, you use .NET attributes to shape each operation’s messages according to the schema you defined earlier. To implement Web services infrastructure, you use a combination of reusable components and SOAP extensions that can be easily applied to any Web service. Finally, if your Web service is for public use, you can register it with UDDI enabling clients to implement UDDI scenarios such as the invocation pattern.