Designing the Contract
June 6, 2001
It has been a long time since we last spoke with you through the At Your Service column. To help remedy this situation, we are putting a team on the project: Matt Powell and Scott Seely. I, Scott, get to go first. Mary Kirtland introduced us a while ago in the Getting to Know Us article. Still, we would like to introduce ourselves a little better, now that we will be present in this space on a regular basis.
Matt has been at Microsoft for over 10 years, most of which he has spent in Microsoft's Developer Support Organization. He insists he's a network protocols guy, since he has had the opportunity to support everything from DLC, NetBIOS, Winsock, RPC, SNMP, WinInet, and ISAPI. Matt prefers the MS-DOS prompt to Windows® Explorer and in a pinch, can still use EDLIN to edit a text file. During his off hours, Matt likes to take his family—all 9 of them—to Seattle Mariners games. He's one of the ones in the first row, right behind Ichiro, waving a white towel and generally trying to keep the Mojo in his Sodo.
If you have read the back issues of this column, you may have noticed that one of our team members, James Francisco, mentioned we were falling behind schedule because half the developers (me) were on paternity leave. On March 15, I got to meet my daughter, Angeline. For four weeks, her mom, big brother, and I spent time getting to know our new family member. I also took that time to finally finish my book on SOAP for Prentice Hall, SOAP: Cross Platform Web Service Development Using XML. It should be on the shelves by the end of July 2001. Besides writing, I am also the designated cross-platform guy. Why? I have actually used three, non-Microsoft SOAP toolkits: my own SimpleSOAP, as well as Apache SOAP, and SOAP::Lite.
In this column, we will take a look at what you need to consider when designing the contract for your Web Service. Hopefully, when you think of a Web Service, you no longer think of things like simple retrieval of stock quotes, or getting the current temperature in your hometown. Instead, you should be thinking of Web Services as components with fairly full-featured APIs. In this column, we look at how to design the interface to the Web Service, as well as any globalization requirements.
Defining the Interface to the Web Service
Today, the way we tell others how to reach and use a Web Service is through a Web Services Description Language (WSDL) file. When SOAP was first introduced, several XML-based interface description languages (IDL) emerged. All of these IDLs recognized different ways of describing a service, and included items specific to the SOAP implementation they were tied to. The industry quickly recognized the need to standardize, and WSDL emerged. WSDL describes the following items:
- The data types recognized by the Web Service
- The message schema
- The exchange method used by the Web Service (request/response, one-way, multicast, and so on)
- The location of the Web Service
- Fault information
- Header information
A complete description of WSDL is beyond the scope of this article, but at the end of the article, you'll find links to some excellent backgrounders on WSDL. For now, however, I want to concentrate on how to use the above items to define your interface to the Web Service.
When defining your interface, you want to first look at your data types. Always make sure your types can map back to XML Schema data types. For example, for strings you should use xsd:string. When you build complex types, make sure they eventually decompose to one of the predefined XML Schema data types. You might define a contact, for instance, by their e-mail address and business phone number. The types section of the WSDL file would look like this:
<definitions xmlns:s="http://www.w3.org/2001/XMLSchema" 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="ContactInfo"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="name" nillable="true" type="s0:PersonName" /> <s:element minOccurs="1" maxOccurs="1" name="emailAddress" nillable="true" type="s0:EmailAddress" /> <s:element minOccurs="0" maxOccurs="unbounded" name="phoneNumbers" nillable="true" type="s0:PhoneNumber" /> </s:sequence> </s:complexType> </s:element> <s:complexType name="PersonName"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="first" nillable="false" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="middle" nillable="true" type="s:string" /> <s:element minOccurs="1" maxOccurs="1" name="last" nillable="false" type="s:string" /> </s:sequence> </s:complexType> <s:complexType name="PhoneNumber"> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="areaCode" nillable="false" type="s:long" /> <s:element minOccurs="1" maxOccurs="1" name="prefix" nillable="true" type="s:long" /> <s:element minOccurs="1" maxOccurs="1" name="suffix" nillable="false" type="s:long" /> </s:sequence> </s:complexType> </s:schema> </types> ... </definitions>
As you can see, all the complex types eventually reduce to an existing XML Schema data type. By using this limitation, you increase the likelihood that other SOAP implementations will be able to interact with your Web Service. Why? Many XML Schema implementations exist for many different platforms; by sticking with standards and avoiding enhancements to your favorite implementation, you increase the usability of your Web Service.
Next, you will want to look at how you factor the functionality of the Web Service. When programming a Web Service, group related operations into a single port. For the Favorites service, we decomposed the design into three services:
- Logon: Authenticates licensees and grants them a key to use when accessing the service
- Account: Handles the creation of end users and the maintenance of Favorites data
- Report: Takes care of reporting
By dividing the functionality into blocks of related functions, you make it easier for users of the Web Service to find the functions they need when working on a particular feature. Programmers will then know that related functionality lives on a particular port, making their lives a little easier.
The implementation of these ports will typically reside within one object. Internally, the objects may share common helper functions. Externally, these objects should not rely on internal state. I have been watching the newsgroups and have seen that many people get frustrated when trying to pass pointers or references around for SOAP objects. Why are they frustrated? Because they design something that requires a pointer reference, and then quickly realize that SOAP does not allow for this to happen.
If you have designed yourself into a corner, a good workaround exists: in/out parameters. Regardless of whether an object is an in, out, or in/out parameter, it must be able to write its state to XML. When you specify the part of the object you need, analyze what data the function really requires, and only demand that information. This can make a significant difference, because transmitting the data over the network can take more time than the entire serialization process. The smaller the message, the less time it will take getting to its destination.
You will also want to make sure that you watch the parameters that go back and forth. If you use a tool that generates the WSDL for you, such as the .NET run time or the SOAP Toolkit v2 WSDLGEN.EXE tool, you may not be aware that you are wasting bandwidth. While working on the solution, take some time to see what the generated WSDL looks like. See if you can find places to trim down the messages going back and forth. For example, any parameter that is sent as ByRef in a Visual Basic® COM object (and they are all ByRef if you do not specify anything) will be an in/out parameter. If you use the object, but do not modify it before return, change that parameter to ByVal instead. Likewise, inspect the IDL files for your C++ objects and make sure the parameters are appropriately decorated.
For the most part, your Web Service will use request/response—traditional RPC. You can specify one-way by omitting the operation's output element.
<operation name='DeleteFavorite' parameterOrder='key Username FavID'> <input message='wsdlns:Account.DeleteFavorite' /> <output message='wsdlns:Account.DeleteFavoriteResponse' /> </operation>
<operation name='DeleteFavorite' parameterOrder='key Username FavID'> <input message='wsdlns:Account.DeleteFavorite' /> </operation>
The one-way message simply omits the output message. When it makes sense, omit the output message. Doing so reduces the amount of time spent waiting, and can make the service appear to be faster since the client will not be waiting for a return message.
Because WSDL is an IDL, the only requirement for the code behind the WSDL port is that it correctly responds to any SOAP requests. What does this mean for you? Let's imagine that we are working on a client application that talks to servers that process orders. One server might process furniture orders, another book orders, and a third DVD orders. Assuming that the only difference between these systems is the database of items, we can use the same client to access these different services. The only difference should be the endpoint of the service. (This is one of the concepts that UDDI relies on.) WSDL's SOAP binding allows this through the use of the soap:address element.
<service name='Account' > <port name='AccountSoapPort' binding='wsdlns:AccountSoapBinding' > <soap:address location='http://coldrooster.com/ssf/account.asp' /> </port> </service>
Many WSDL-aware toolkits allow you to change the endpoint after reading in the file. If you know the location of the other services that implement the port, or if you use a UDDI server to get similar information, you can use the same client to talk to different endpoints.
By taking some time to think about your SOAP interface, your Web Service should be accessible from any machine. If several sources offer the same service, a common interface allows a single client to access the various Web Services. When designing these applications using automated WSDL generation tools, take some time to look at the generated WSDL and make sure that you are only sending what is absolutely necessary. For short operations, you will find that the network is the bottleneck.
Globalization of the Web Service
You will also need to consider what kind of reach the Web Service will have. People all over the world could be using the service: What does this mean to you?
You might have issues with data storage and transmission. If your service accepts string data, make sure you can accept, transmit, and store international alphabets (for instance, Japanese, Cyrillic, Arabic, and so on). With the Favorites service, we handled this by requiring that the service could accept UTF-8 and UTF-16 encoded XML. The Microsoft® SOAP Toolkit v2 handles this requirement for us. You also need to think about large character sets when considering your back-end systems. Behind the scenes, use Unicode-aware string types and data storage. For Favorites, we used Visual Basic, because it handles Unicode strings natively. On the storage end, we store all of our strings in Microsoft® SQL Server using the nchar and nvarchar data types. As you can see, picking the right tools can make it easy to implement this aspect of internationalization.
You also need to consider how you distribute the Web Service documentation. Most developers can read and write in English, allowing you to achieve significant reach by providing good English documentation. In these documents, do not use slang, and keep the English as simple as possible. To increase acceptance of your service in other countries, you will want to consider providing documentation in the native language of the developers.
When returning SOAP faults, you might want to consider allowing licensees to specify a preferred language for the Fault.Description member. A good number of your errors might be suitable for viewing by the end user of the service. We gave some thought to providing the error strings in specific languages but decided against doing so. (I understand that Project Lucy is looking into translating the error strings—hopefully there will be a discussion on this from their team later this year.) Instead, we wrote documentation specifying the Fault.Code values that could be returned by each and every function exposed by the Web Service. This allows programmers implementing applications that use the Favorites service to handle errors and localization on their own.
You also need to consider what to do when your Web Service starts gaining popularity outside your own region. For the moment, Cold Rooster's services all reside in Redmond, WA. Users outside of the Americas have to deal with a transoceanic Internet link to use the service. If the numbers justified it, we would have to look at deploying the Web Service in other parts of the world, so that users could get faster response times. With globally distributed servers handling the requests, new issues begin to rear their heads. For one, when expanding the Web Service, we would have to consider how we distribute the user data.
For the Favorites service, here are just some of the issues that would show up:
- Licensees expect to be able to access the Web Service on all our Web servers, so we will need to devise ways to synchronize the data between data centers.
- End users expect their data to travel with them wherever they are. This means we either force the licensee to remember which data center they contact to get end-user data, or propagate all user data to all data centers.
- How do we handle audit logs when the audit events happen globally? Most likely, we will store all of the audit data in one database in one location and simply queue the requests to that data source in order to avoid any latency issues.
- We will have to devise rules to resolve conflicting changes between data centers scattered around the globe. For example, a licensee might change the administrative contact data on two different servers, and we would have to come up with rules on how to handle the conflict.
A final item relating to globalization, and one easily overlooked, is that of deciding on how to accept payment. Do you allow users to pay in any currency, or do you require them to pay in your local currency? This aspect of globalization can be a complex issue, and we are putting it in the idea bin for a future article, depending upon reader interest.
When designing a Web Service, consider how the Web Service will be used. You can optimize its performance on the network if you only take the time to look at the WSDL file that your tools generate for you. These files will help you pinpoint where you are passing large amounts of data that can hurt overall throughput. Also, avoid placing all of your functionality in one bucket unless it makes sense to do so. Group related functions on a port.
When thinking about globalization, try to design for a wide audience. Expect that audience to speak a variety of languages. For the Favorites service, we tested to make sure we could store strings for languages with large alphabets (like Chinese). You should do the same when testing your Web Service. Identify all strings that might be outside the first 128 ASCII characters, and make sure you can store and retrieve strings in Chinese, Arabic, and a few other alphabets. This checks how well you handle Unicode strings.
For further reading on WSDL, check out the following articles:
You can see a complete list of declared XML Datatypes in XML Schema Part 2: Datatypes.