Give Your Web Services Consumers the Exact XML They Need to Succeed
This article discusses:
|This article uses the following technologies:
XML, Web services, and Visual Basic
Code download available at:WSE20.exe(141 KB)
The Big Easy
When Good Messages Go Bad
The Decorative Way
The Backdoor Way
The WSE Way
Practice Makes Perfect
Web services are first and foremost about messages. No matter what platform you are using, what industry you are building services for, or what levels of complexity are available in your Web services stack, it all comes back to messages. When you are developing services or their corresponding clients, you are creating, transporting, and processing XML messages. Because tools try to make that process easy, sometimes you might (conveniently) forget what it's really all about.
No matter how good your Web services tools are, there will inevitably be those moments when something doesn't work, and you find yourself brought back down to earth and staring at the content of your messages. The more sophisticated the service, the more complex the messages, and the more likely that you will find yourself in this situation. It is at that moment that you want more from your tools than just a good way of tracing messages. Developers building sophisticated Web services will inevitably need to take control over the content and structure of their messages.
The Microsoft® .NET Framework has always provided good support for XML, the serialization of objects as XML, and support for Web services in general. For support of the latest Web services specifications, like WS-Addressing or WS-Security, however, developers must use the Web Services Enhancements (WSE) for .NET. The latest version of WSE (WSE 2.0) provides its own mechanism for custom message handling, in addition to supporting the hooks already in place in .NET. This article will discuss each of the methods available to developers for custom message serialization and will cover in detail the method used in WSE 2.0.
WSE 2.0 makes it possible for developers to build Web services and related client applications that possess, as the name suggests, enhanced features that the rest of the current .NET Web services infrastructure does not provide. For example, WSE provides message-level security through an implementation of the WS-Security specification, message routing through WS-Addressing, and service policy control through WS-Policy. While WSE gives developers the ability to use all of these enhanced features, at its core WSE 2.0 uses a simple messaging model that does not change, regardless of how many enhancements you take advantage of.
The EndpointReference class is the WSE implementation of the WS-Addressing concept of an endpoint reference, which describes a Web service in terms of its address (both logical and physical) and additional properties. An EndpointReference class represents a Web service endpoint, which represents the place the messages should be sent. This could be either an intermediary or the ultimate destination of the message, but in any event it is used throughout WSE to indicate the target of a message.
To create an EndpointReference, you may provide a URI that defines the address of the service. In the following lines of code you can see how to create an EndpointReference using an instance of the Uri class in Visual Basic® .NET:
Dim destUri As Uri = New Uri("http://www.example.com/MyService.asmx") Dim destination As EndpointReference = New EndpointReference(destUri)
The EndpointReference is used throughout WSE 2.0 to indicate where a client should send messages, or where a service endpoint will be listening for them. The use of the EndpointReference instead of just a URI is significant for two reasons. First, it allows you to define an endpoint in terms of both a logical and physical address, something that was not possible before in .NET Web services. Second, the EndpointReference class is significant because it is an excellent example of WSE's custom serialization hooks at work (as you'll see later).
For a more complete discussion of the enhanced features of WSE 2.0, such as WS-Security, take a look at Aaron Skonnard's article from the August 2004 issue of MSDN®Magazine, "What's New in WSE 2.0". If you are looking for more information on EndpointReference and the move towards WS-Addressing in WSE 2.0, you can also take a look at Aaron's article on WS-Addressing and WSE 2.0, "Moving From WS-Routing to WS-Addressing Using WSE 2.0".
Web Services Enhancements provides two levels of messaging objects, allowing developers to work at either a low (raw XML) or high (objects as messages) level. Whichever level of messaging you choose to do with WSE, each individual message is contained in an instance of the SoapEnvelope class.
The SoapEnvelope class allows direct access to the SOAP message in its raw form. For developers new to SOAP or Web services, the SOAP envelope is a wrapper around the message payload that can also contain one or more header blocks. One very interesting feature of SoapEnvelope is that it derives from the System.Xml.XmlDocument class. This means that WSE allows you to manipulate your raw messages in exactly the same way you are probably familiar with: using the properties and methods of the XmlDocument class. Because the message is already loaded into an XmlDocument, you can also do things like execute XPath expressions or test the namespaces used in the message using the same methods you would use on any other XML document:
' Load a message from a file, using XmlDocument.LoadXml Dim envelope As SoapEnvelope = New SoapEnvelope() envelope.LoadXml( "c:\soapmessages\soap.xml" )
For those developers with a true passion for XML, or those who feel they must have complete control over their messages, WSE 2.0 provides a low-level API for sending and receiving messages. There are two objects that are used to handle messages at this level: SoapSender and SoapReceiver. Both of these objects work with messages as raw XML via the SoapEnvelope class.
Once you have a message to send in the form of a SoapEnvelope, you can use the SoapSender class provided by WSE 2.0 to send the message. To listen for SOAP messages at a particular endpoint, the client can also derive from the SoapReceiver class. The following code illustrates a brief example of how a Web service client can be created using these two objects:
Dim destUri As Uri = New Uri("http://www.example.com/MyService.asmx") Dim destination As EndpointReference = New EndpointReference(destUri) Dim sender As SoapSender = new SoapSender(destination) ' Fill the contents of the envelope, including any addressing Dim envelope As SoapEnvelope sender.Send(envelope)
It would seem that any discussion of custom serialization of messages in WSE would end here. If you can construct or process your messages as raw XML documents, what more could you ask? For one thing, you could ask to not have that required of you. A great deal of work has gone into making it possible for developers to work with Web services in a way that is familiar to them—as objects. While this may hide some of the details from developers, it also provides a more productive environment for many developers to work in. In addition, if your messages are being treated as raw XML documents, you have done nothing to create a description of your Web service—in other words, Web Services Description Language (WSDL) and associated schema. Without a service description, the contract between service and client is open ended and able to accept any XML as a message.
Not everyone loves XML enough to build their messages by hand, and the WSE team appreciates that fact. For those developers who feel more comfortable using the .NET Framework to convert between their objects and XML messages (the model that developers who use ASP.NET Web services will find familiar), WSE 2.0 also provides objects to build clients and services. These are the SoapClient and SoapService classes, respectively. Like SoapSender and SoapReceiver, these classes are responsible for sending and receiving messages, but they take it a step further. These classes combine features that make it easier for developers to use SOAP messages in the request-response model that is commonly used when working with Web services over HTTP.
Consider the service defined in Figure 1 which derives from SoapService and which most likely looks more familiar to developers than the earlier example that used SoapSender. Internally, SoapService is using both a SoapReceiver and a SoapSender to make the request-response message exchange pattern happen, but it does much more than that. You will notice that there is no longer a SoapEnvelope in sight. Instead, you are working with objects as both parameters and return type.
Figure 1 SoapService at Work
Imports System.Xml Imports System.Xml.Schema Imports System.Xml.Serialization Imports Microsoft.Web.Services2 Imports Microsoft.Web.Services2.Addressing Imports Microsoft.Web.Services2.Messaging Imports Microsoft.Web.Services2.Xml Public Class MyWseService Inherits SoapService <SoapMethod("http://tempuri.org/LookupEmployee")> _ Public Function LookupEmployee(name As String) As MyEmployee Dim myEmployee As MyEmployee = New MyEmployee() ' Find the employee information and populate ... Return myEmployee End Function End Class
It's nice that WSE 2.0 provides this familiar model, but how does it work? How does WSE translate from an employee class I have defined to an XML message and back again? The answer is the same for both WSE and ASP.NET Web services: XML serialization.
The Big Easy
Whether you are creating Web service applications with ASP.NET or with WSE, XML serialization is used to convert objects into XML messages and back again. The System.Xml.Serialization namespace of the .NET Framework provides a number of classes that handle the object-to-XML conversion process. These objects are not just available to the .NET internals; you can use them in any application, and it helps to have a level of familiarity with them since they are used on your behalf every time you consume or create a Web service with .NET.
In general, the serialization of objects to XML (and vice versa) is taken care of automatically by these objects. The key object in the XML serialization process is the aptly named XmlSerializer.
The XmlSerializer class is responsible for converting between XML and instances of objects. XmlSerializer will convert an instance of a class to an XML element of a given name and namespace. It then uses reflection to examine all the public data members of a class, and it maps each of those to a child element of the original element. Figure 2 shows a simple class definition and its corresponding XML serialized structure once it has passed through the XmlSerializer.
Figure 2 MyEmployee Before and After
Public Class MyEmployee Public Name As String Public DOB As DateTime Public IQ As Integer End Class
<MyEmployee> <Name>John Q. Public</Name> <DOB>7/4/1976</DOB> <IQ>150</IQ> </MyEmployee>
If you want to use an XmlSerializer with your SoapEnvelope, you can. The following code sheds some light on what is really happening under the covers of the Web services stacks:
Dim xs as System.Xml.Serialization.XmlSerializer xs = New XmlSerializer(GetType(MyEmployee)) Dim envelope As SoapEnvelope = New SoapEnvelope() ' Envelope gets populated from the Web service call Dim nodeReader as XmlNodeReader = _ new XmlNodeReader(envelope.Body.FirstChild) employee = xs.Deserialize(nodeReader) As MyEmployee
Since your SoapEnvelope is an XmlDocument, you can construct any number of XmlReader or XmlWriter objects on top of it and pass them to the Serialize or Deserialize methods, respectively. Now that you have a better idea about how XML messages and objects are related, let's take a look at some more complex messages and see where custom serialization becomes necessary.
When Good Messages Go Bad
The default behavior of XML serialization through XmlSerializer can take you through many cases, but sometimes you need to take more control over the way in which your objects are serialized. The class provided here illustrates two potential problems commonly encountered when working with XML serialization of objects:
Public Class MyService Public States As StringCollection = New StringCollection() Public ServiceUri As Uri End Class
In the first case, you have a StringCollection that represent names or abbreviations of the states (for example, North Carolina). There are a number of ways that you could serialize such a list but which one should the XmlSerializer choose? In this case, you would need to provide more information. The second issue is a bit trickier because one of the members of the class is of the System.Uri type, and the System.Uri class has no default constructor (a public constructor that accepts no parameters). Classes that have no such constructor cannot be deserialized from XML because the XmlSerializer class does not know how to instantiate one, and if it can't be deserialized, XmlSerializer will also refuse to serialize it. Because of this, the XmlSerializer would also fail to serialize the MyService class because ServiceUri happens to be a public member.
The default behavior of XML serialization can take you through most cases, but sometimes you need to take more control over the way in which objects are serialized. In addition to handling messages as raw XML, as you saw previously, there are three other methods at your disposal that you can use to shape the content of your Web services messages: attributes, IXmlSerializable, and IXmlElement.Implementing IXmlElement
While WSE provides the OpenElement class and other similar classes to use for custom serialization, it is the IXmlElement interface that is the hook. If you want, you can implement IXmlElement in your own classes and achieve much the same results as if you had derived from OpenElement. If you do choose to implement IXmlElement yourself instead of deriving from a WSE base class, you must remember to provide a special constructor for your object.
This special constructor must take as a parameter an instance of the XmlElement class, the same parameter that you would pass to the IXmlElement.LoadXml method. This constructor can be nothing more than a pass-through to the LoadXml method, as shown here:
Public Class MyMessageClass Implements IXmlElement ' IXmlElement implementation goes here... Public Sub New( element As XmlElement ) Me.LoadXml( element ) End Sub End Class
Without this constructor, your message objects will appear to be serialized to XML successfully, but they will fail when WSE attempts to deserialize them. The reason for this is that the WSE code assumes that message objects that implement IXmlElement also implement this special constructor (since all the WSE classes do).
Instead of creating the object with a default empty constructor and calling LoadXml, it simply constructs the object using the XmlElement. The thing to remember here is to derive from a WSE class like OpenElement, or provide a special constructor in your own IXmlElement implementation.
The Decorative Way
There are times, of course, when you need to step in and control the behavior of the serialization process. The first method that a developer can use to control the XML serialization process is through attributes. The System.Xml.Serialization namespace provides a number of attributes that can be used to change normal serialization behavior. Figure 3 lists a few of the more common attributes and the effect that each of them has on the serialization process. The full list is available in the documentation for the System.Xml.Serialization namespace.
Figure 3 Sample of XML Serialization Attributes
|XmlRootAttribute||Public class declarations||Sets the element name and namespace used by a root element|
|XmlIgnoreAttribute||Public properties and fields||Serialization skips the member entirely|
|XmlAttributeAttribute||Public field, property, parameter, or return value||Serializes the member as an attribute instead of as an element|
|XmlArrayAttribute||Public field, property, parameter, or return value that returns an array of complex objects||Serializes the array as an XML array|
Whenever a class that uses one or more of these attributes is serialized, the XmlSerializer class uses reflection to determine what attributes are being used and how they should be interpreted. In the class shown here, I have added attributes to the previously defined class in order to declaratively shape the XML serialization, as shown in Figure 4.
Figure 4 Adding Attributes
<XmlRoot(ElementName:="MyService", Namespace:="http://tempuri.org")> _ Public Class MyService <XmlArray(ElementName := "States", _ Namespace := "http://tempuri.org/states/", _ IsNullable := True)> _ Public States As StringCollection = New StringCollection() <XmlIgnore()> _ Public ServiceUri As Uri End Class
The XmlRootAttribute describes the MyService class as a whole, the XmlArrayAttribute describes how the States collection should be serialized as a list of items, and the XmlIgnoreAttribute states that the Uri member ServiceUri should be skipped completely during serialization. With those changes, the class can now be successfully passed to XmlSerializer. The XML that is created when this object is serialized looks like this:
<MyService> <States> <Item>NC</Item> <Item>NV</Item> <Item>VA</Item> </States> </MyService>
Although using the attributes to shape XML messages is straightforward, it cannot fix every problem. Most obviously, there is still no serialized information about the Uri member, which may or may not be a problem depending on the class design. On the other hand, suppose you did not like how the States list was serialized. You may still be unable to get what you want from attributes. For example, there are no attributes available that can tell the XmlSerializer to write an array of strings as an XML type of xsd:list, as shown in the following:
<MyService> <States xmlns="http://tempuri.org">NC NV VA</States> </MyService>
This is a common enough construct in XML and one that is well suited to representing a list of items, but that does not require the overhead of an element for each item. For that reason, elements of the xsd:list type appear in many of the messages used for the latest Web services specifications, like WS-Discovery. In order to format the list of strings as an xsd:list, you would need to take more control over the XML serialization process.
The Backdoor Way
Another way to take more control is by implementing the IXmlSerializable interface, which is a mechanism for overriding the XML serialization process:
Public Interface IXmlSerializable Sub WriteXml(writer As System.Xml.XmlWriter) Sub ReadXml(reader As System.Xml.XmlReader) Function GetSchema() As System.Xml.Schema.XmlSchema End Interface
The IXmlSerializable interface has three methods: two for serialization and one for schema generation. The WriteXml and ReadXml methods are invoked whenever the object in question is being serialized or deserialized. These methods take an XmlWriter instance and XmlReader instance respectively, types which are probably familiar to developers who have been working with XML in the .NET Framework. Just as you could use familiar XmlDocument methods when working at the SoapEnvelope level, you can rely on methods like XmlWriter.WriteStartElement in order to create your message. The GetSchema method returns an instance of System.Xml.Schema.XmlSchema, the .NET type that represents an XML Schema document. This method is called whenever schema information is needed about the object, such as during the construction of WSDL for the Web service.
Using this method takes care of some of the issues you might encounter with XML serialization. For example, if your object implements IXmlSerializable, you can provide public properties and members that are non-serializable data types without generating exceptions from XmlSerializer. This would allow you to take care of the Uri member that you could not serialize earlier.
Figure 5 shows an example of how you can serialize a collection of strings as an xsd:list type. This code is written to serialize a generic list, but the SerializableList class can serve as a base class, and by initializing the ListName and ListNamespace members in the constructor, you can further customize your serialization.
Figure 5 Serializing a String Collection as an xsd:list Using IXmlSerializable
' This class serves as an example of how you could use ' the IXmlSerializable interface to serialize a StringCollection as an ' xsd:list. This technique will not work for Web services in the ' .NET Framework 1.1 and WSE 2.0 because of the issues with ' combining schemas in the service description, but it could still be ' used in other situations where XML serialization is used in .NET. Public Class SerializableList Implements IXmlSerializable Public Sub New() Me.ListName = "SomeList" Me.ListNamespace = "http://www.thoughtpost.com/2004/xml" End Sub Public Sub New(ByVal ListName As String, _ ByVal ListNamespace As String) Me.ListName = ListName Me.ListNamespace = ListNamespace End Sub Public Items As StringCollection = New StringCollection Public ListName As String = String.Empty Public ListNamespace As String = String.Empty Public Function GetSchema() As System.Xml.Schema.XmlSchema + Implements System.Xml.Serialization.IXmlSerializable.GetSchema Dim xsd As XmlSchema = New XmlSchema xsd = New System.Xml.Schema.XmlSchema xsd.Namespaces.Add("xsd", "http://www.w3.org/2001/XMLSchema") xsd.Namespaces.Add("xsi", _ "http://www.w3.org/2001/XMLSchema-instance") xsd.Namespaces.Add("tns", Me.ListNamespace) xsd.Id = Me.ListNamespace xsd.TargetNamespace = Me.ListNamespace Dim elem As XmlSchemaElement = New XmlSchemaElement elem.Name = Me.ListName Dim stype As XmlSchemaSimpleTypeList = New XmlSchemaSimpleTypeList elem.SchemaTypeName = New System.Xml.XmlQualifiedName( _ "list", "http://www.w3.org/2001/XMLSchema") xsd.Items.Add(elem) Return xsd End Function Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) _ Implements System.Xml.Serialization.IXmlSerializable.ReadXml Me.ListName = reader.Name Me.ListNamespace = reader.NamespaceURI Dim data As String data = reader.ReadElementString() If ((data <> Nothing) AndAlso (data.Length > 0)) Then Dim delims(1) As Char delims(0) = " " Dim items() As String items = data.Split(delims) For Each s As String In items If ((s <> Nothing) AndAlso (s.Length > 0)) Then Me.Items.Add(s) End If Next s End If End Sub Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) _ Implements System.Xml.Serialization.IXmlSerializable.WriteXml Dim data As StringBuilder = New StringBuilder() For i As Integer = 0 To Items.Count data.Append(Items(i) & " ") Next writer.WriteElementString(Me.ListName, Me.ListNamespace, _ data.ToString().Trim()) End Sub End Class
It would seem that implementing the three methods of IXmlSerializable would take care of every issue developers might have with their messages, but there are a couple of problems that prevent that from being the case. Originally, the IXmlSerializable interface was not recommended for use outside of the .NET Framework classes. The documentation stated that it was meant for internal use only, and so developers who chose to use it knew that it was a technical risk. Since then, it has become clear that its use will be supported in the .NET Framework 2.0, so you can use it to do custom serialization without worrying about the interface changing in the next version. However, you will likely encounter problems with its use in the current version of .NET, especially when it comes to working with Web services.
Such a problem can be reproduced by trying to use an object that implements IXmlSerializable with WSE 2.0. The problem lies in the way that XML serialization is used by a Web services stack like WSE. In order for a message to be used by the service, it must be described as part of the description of the service itself, namely its WSDL document. The WSDL documents contain embedded schema information about messages, and this is where the problem appears. Through IXmlSerializable, an object can only return a complete schema (not a portion) through the GetSchema method, and the Framework has problems with combining schemas when generating the service description. If your message object is of a different XML namespace than the service, duplicate schema information is generated in the subsequent WSDL file (one for the correct XML namespace, and one for the service). If your message object is of the same XML namespace as the service, then the WSDL generation fails because the document can't contain the same namespace twice. In either of these cases, you do not get the behavior that you are looking for.
It is worth mentioning that in the .NET Framework 2.0, this problem is solved by allowing the object to return an element of a schema instead of an entire schema document, which makes it possible to merge schemas during the WSDL generation step. For more information on this change and on IXmlSerializable in the .NET Framework 2.0, see the MSDN online article "New Features for Web Service Developers in Beta 1 of the .NET Framework 2.0".
The WSE Way
WSE 2.0 introduces its own mechanism for custom serialization: the IXmlElement interface. This interface is defined in the Microsoft.Web.Services2.Xml namespace, and by implementing this interface, an object can override the XML serialization process.
Like IXmlSerializable, the IXmlElement interface defines methods for creating an object from XML, as well as for retrieving an object's state as XML. A definition of the IXmlElement interface is shown in the following code snippet:
Public Interface Microsoft.Web.Services2.Xml.IXmlElement Function GetXml(ByVal document As XmlDocument) As XmlElement Sub LoadXml(ByVal element As XmlElement) End Interface
In fact, the IXmlElement interface is implemented by a number of WSE 2.0 objects. Objects that have a complex structure and need to be represented in a message implement IXmlElement in order to handle the serialization process in a custom manner. The best example of one of these objects is the EndpointReference class, which I discussed earlier in the article, and is used throughout the entire object model. If I try to define a Web service method that returns an EndpointReference, or even create my own message object that contains a member of the EndpointReference type, the service will fail. The reason for this is that EndpointReference does not have a constructor that takes no parameters. If a class has no empty constructor, then it cannot be serialized by the Framework because it does not know how to create one. So how does WSE manage to get EndpointReference instances serialized in your messages? It first checks to see if an object implements IXmlElement before passing it to an instance of XmlSerializer. If it does implement the interface, then it uses the LoadXml and GetXml methods to handle serialization; if it doesn't, then the object goes the same route as all of the methods, which is through XmlSerializer.
It may look as though IXmlElement is the answer to handling complex messages within WSE, but there are still two problems. First, if you read the WSE 2.0 documentation, IXmlElement is described in much the same way as IXmlSerializable in that it supports the .NET Framework infrastructure and is not intended to be used directly from your code. Second, there is no method in IXmlElement for providing schema information about your custom object. Where IXmlSerializable had the GetSchema method, IXmlElement has nothing. In some cases, you could probably live without describing your object in schema, but remember that you are dealing with Web services in this case. Without schema information, it is difficult for the XML serialization model to determine how your object should be represented in the WSDL that describes the service. Luckily, the WSE team provided a set of classes in the Microsoft.Web.Services2.Xml namespace that solve these two problems and can be used to build complex message objects. The OpenElement class is a good example of one of these support classes, and it can serve as a base class for complex messages.
So how does deriving from OpenElement solve the schema generation problem? Any classes that derive from OpenElement (or the other similar classes in the WSE Microsoft.Web.Services2.Xml namespace) generate schema with an open content model. For those not familiar with XML Schema, an open content model indicates that the XML type in question can accept a number of additional elements or attributes beyond what is strictly described in the schema. Required items are still required, but it is not an error if additional information exists. This opens a sort of back door to the validation process, saying that this type will accept anything (or some subset of anything, depending on what wildcards you use). It is then up to your custom serialization code to decide what to do with this additional content, if anything. OpenElement is an abstract class; because it derives from IXmlElement but does not implement the serialization methods, the derived class must do this.
This technique gives developers a nice middle ground in terms of flexibility versus structure. Objects that have a complex structure, one that is difficult to automatically serialize to XML, can handle their own serialization, describe as much of their structure as they reasonably can, and still describe themselves using an open content model to allow for additional items that are hard to describe. A nice side effect of this approach is the support this technique provides for versioning of message information. By describing the message with an open content model, a service can allow the message structure to grow somewhat over time without requiring changes to the service contract. The ability to process that additional content through the IXmlElement methods comes in handy at that point.
Figure 6 shows an example of the earlier message class, now implemented by deriving from OpenElement. This class now handles all of the custom serialization issues. The Uri is replaced with an EndpointReference, but the structure is inherently the same, as are the issues involved. This class is included in a set of sample programs where WSE custom serialization methods are used to send and receive complex messages. You can download the entire set of samples from the MSDN Magazine Web site, where the code is available in both C# and Visual Basic .NET. (See the sidebar Implementing IXmlElement.)
Figure 6 MobileService Sample Message Class
' MobileService, the complex message returned by our sample services, ' uses a combination of XML attributes (ex. XmlRootAttribute, ' XmlIgnoreAttribute, etc.) and WSE's IXmlElement hooks to generate a ' complex message. <XmlRoot(ElementName:="MobileService", _ Namespace:="http://www.thoughtpost.com/2004/MSDN/WSEXML")> _ Public Class MobileService Inherits OpenElementElement Public Sub New() End Sub Public Sub New(ByVal element As XmlElement) Me.LoadXml(element) End Sub ' Rename Name to LocationName <XmlElement(ElementName:="LocationName")> _ Public Name As String ' Skip the EndpointReference; XML serialization can't handle it ' because of the lack of an empty constructor <XmlIgnore()> _ Public EndpointLocation As EndpointReference = Nothing ' Our additional IXmlElement implementor. ' WSE drills down, so messages can implement IXmlElement, but their ' members can as well (see sample code). Public States As StateList = New StateList Public Overrides Function GetXml(ByVal document As _ System.Xml.XmlDocument) As System.Xml.XmlElement Dim element As XmlElement element = document.CreateElement("MobileService", _ "http://www.thoughtpost.com/2004/MSDN/WSEXML") Dim nameelem As XmlElement nameelem = document.CreateElement("LocationName", _ "http://www.thoughtpost.com/2004/MSDN/WSEXML") nameelem.InnerText = Me.Name element.AppendChild(nameelem) ' It's easy to include members in the serialization process if ' they too implement IXmlElement, such as EndpointReference Dim endpoint As XmlElement endpoint = Me.EndpointLocation.GetXml(document) element.AppendChild(endpoint) Dim stateelem As XmlElement stateelem = Me.States.GetXml(document) element.AppendChild(stateelem) Return element End Function Public Overrides Sub LoadXml(ByVal element As System.Xml.XmlElement) Me.Name = "" Me.EndpointLocation = Nothing Me.States.Items.Clear() Dim node As XmlNode For Each node In element.ChildNodes Select Case (node.LocalName) Case "EndpointReference" Dim endpointelem As XmlElement = node Me.EndpointLocation = _ New EndpointReference(endpointelem) Case "LocationName" Me.Name = node.InnerText Case "States" Dim stateelem As XmlElement = node Me.States.LoadXml(node) End Select Next End Sub End Class
Practice Makes Perfect
You probably won't need to take control of the serialization process for every message you send, but it helps to be familiar with the hooks when you do encounter messages that WSE 2.0 and the XML serialization process just don't approve of. Some of these techniques can also be used outside of Web services.
For example, you can use attributes or IXmlSerializable to control how your objects should be serialized in XML format to a database or file. The new interfaces and classes in the WSE 2.0 XML namespace provide support only within WSE Web services, but inside that domain, the support they provide is flexible enough to support the latest Web service specifications. If this technique is good enough to do that, it is good enough to handle just about any SOAP message your application can cook up to send out over the wire to any other Web service or application.
Chris Dix is the founder of Thoughtpost, a consulting and training company in Charlotte, NC, where he focuses on the .NET Framework and Web services. Chris is a frequent speaker at industry conferences, and he is the co-author of several books, including Professional XML Web Services (Wrox Press, 2001). You can contact Chris at firstname.lastname@example.org.