WCF Messaging Fundamentals
Explore the code.
Download the code.
Get the sample code for this article.
NEW: Explore the sample code online!
- or -
Code download available at: ServiceStation2007_04.exe (161KB)
XML Representations
The Message Class
Message Versions
Reading and Writing Messages
Typed Message Bodies
Message Lifetime
Message Headers and Properties
Mapping Messages to Methods
Endpoints and Bindings
Bringing It All Together
W
hen you start pulling the layers away from Windows® Communication Foundation, you find a sophisticated XML-based messaging framework that offers a world of possibilities when connecting systems using a variety of protocols and formats. In this month's column, I am highlighting some of the key messaging features we can thank for such flexibility.
This column assumes a basic understanding of the Windows Communication Foundation programming model. If you're new to it, before proceeding you should read my article in the February 2006 issue of MSDN®Magazine.
One of the primary goals of the Windows Communication Foundation messaging architecture is to provide a unified programming model while also allowing flexibility in how data is represented and messages are transmitted. It accomplishes this by building on XML as the data model plus SOAP and WS-Addressing as the messaging framework. However, the fact that Windows Communication Foundation builds on these models doesn't mean you have to use XML 1.0, SOAP, or WS-Addressing when transmitting messages. You'll see that Windows Communication Foundation provides a great deal of flexibility.
Since the early days of XML, the software industry has relied on a little-known specification that provides a standard definition for the data found in an XML document. This specification, referred to as the XML Information Set (InfoSet), defines elements and attributes in terms of the information they contain, in a way that's completely independent of the byte representation. (See www.w3.org/TR/xml-infoset for details.)
The InfoSet specification has made it possible for other XML specifications and APIs to offer a consistent view of the data found in an XML document even though they might represent that data completely differently. Ultimately, the InfoSet provides a common rendezvous point for applications working with XML data, as illustrated in Figure 1. In the end, it's the job of an XML processor to translate between the byte representation and the programming model experience.
Figure 1 The Role of the XML InfoSet (Click the image for a smaller view)
Figure 1 The Role of the XML InfoSet (Click the image for a larger view)
Windows Communication Foundation introduces some fundamental enhancements to the System.Xml namespace that make it possible to leverage alternate byte representations-rather than simply literal XML 1.0-when reading and writing XML documents. The main classes of interest here, System.Xml.XmlDictionaryReader and System.Xml.XmlDictionaryWriter, are located in the new System.Runtime.Serialization assembly that ships with the Microsoft® .NET Framework 3.0.
Both the XmlDictionaryReader and XmlDictionaryWriter classes provide static factory methods for creating readers and writers that use text, binary, and MTOM (Message Transmission Optimization Mechanism) representations. For instance, XmlDictionaryReader provides the CreateTextReader, CreateBinaryReader, and CreateMtomReader methods while XmlDictionaryWriter provides the corresponding CreateTextWriter, CreateBinaryWriter, and CreateMtomWriter methods.
Let's look at some examples. Consider this literal XML 1.0 representation of a customer:
<!-- customer.xml --> <Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer>
The following code shows how to read customer.xml into an XmlDocument object and how you can use XmlDictionaryWriter to persist the same XmlDocument object back out to a binary representation on disk:
// read from XML 1.0 text representation XmlDocument doc = new XmlDocument(); doc.Load("customer.xml"); // write to binary representation FileStream custBinStream = new FileStream( "customer.bin", FileMode.Create); using (XmlWriter xw = XmlDictionaryWriter.CreateBinaryWriter( custBinStream)) { doc.WriteContentTo(xw); }
After this code runs, you'll have a binary representation saved in customer.bin that looks like Figure 2. Even though the binary editor shown in Figure 2 happens to display a sequence of characters, this is actually a binary file and obviously does not look like an XML 1.0 file. Nevertheless, it does represent an XML document (an InfoSet). Still, this binary format is proprietary to version 3.0 of System.Xml and is not compatible with other .NET versions or Web service frameworks.
Figure 2 Binary Representation of Customer XML Document (Click the image for a smaller view)
Figure 2 Binary Representation of Customer XML Document (Click the image for a larger view)
The code sample in Figure 3 demonstrates how you can read the binary representation back into an XmlDocument object and how you can persist it back out to an MTOM representation on disk.The MTOM representation looks very different than the text or binary representations, as you can see in Figure 4. Nevertheless, it represents the same XML document and can be processed using the same System.Xml APIs. The MTOM representation also differs from the binary representation in that MTOM is a W3C Recommendation based on the InfoSet and is widely supported across Web service frameworks. MTOM makes it possible to optimize the transmission of binary elements within XML documents by leveraging multi-part MIME framing.
Figure 4 MTOM Representation of Customer XML Document (Click the image for a smaller view)
Figure 4 MTOM Representation of Customer XML Document (Click the image for a larger view)
We can now come full circle by reading the MTOM representation back into an XmlDocument and by persisting it to a text representation on disk, like this:
// read from MTOM representation XmlDocument doc = new XmlDocument(); FileStream custMtomStream = new FileStream( "customer.mtom", FileMode.Open); using (XmlReader xr = XmlDictionaryReader.CreateMtomReader( custMtomStream, Encoding.UTF8, XmlDictionaryReaderQuotas.Max)) { doc.Load(xr); } // write to text (XML 1.0) representation doc.Save("customer.xml");
The resulting custom.xml file should look identical to the original text representation. As you have seen, these System.Xml enhancements offer great flexibility by providing three ways to represent XML for persistence and transmission. Of course, other representations could be added in the future. And although it was Windows Communication Foundation that brought these enhancements to the table, you can make use of them in any .NET Framework 3.0 applications.
Windows Communication Foundation builds on this by letting you choose which XML representation to use when transmitting messages. If you need interoperability, you should choose the XML 1.0 text representation. If you need interoperability along with efficient support for binary payloads, you should choose the MTOM representation. And in scenarios that are .NET-only, the binary representation may provide better performance. The key here is that you actually have a choice.
Another key feature of any messaging framework is the ability to extend message payloads with arbitrary headers. A header is simply extra information traveling with the message, used to implement additional message-processing functionality (like security, reliable messaging, and transactions). In the case of XML messages, this means extending an XML payload with XML headers-both both of these are represented as XML elements framed within a container element. This functionality is exactly what SOAP provides.
The SOAP framework makes it possible to define XML-based protocols that can be used over any transport, but without relying on any transport-specific feature. WS-Addressing is a specification that extends SOAP to provide a transport-neutral mechanism for addressing/routing SOAP messages. Both SOAP and WS-Addressing are based on the InfoSet and permit (but do not require) XML 1.0 syntax to be used when transmitting messages.
The Windows Communication Foundation supports the use of any of the XmlDictionaryReader/Writer representations discussed in the previous section, which allows applications to seamlessly span a range of reach and performance requirements. Windows Communication Foundation models all messages using the Message class shown in Figure 5. As you can see, the Message class essentially models a message body and collections of message headers and properties. The available methods are primarily for creating messages, reading and writing the message body, and manipulating the collections of headers and properties.
You create a Message object by calling one of the various static CreateMessage overloads and you dispose of a Message object using IDisposable or by explicitly calling Close. You can create a new Message from scratch, which is typical when sending a message. Or you can create a new Message from a message stream, which is typical when receiving messages.
In the case of creating a message from scratch, you must specify the action, the message version, and the body to use within the message. The action uniquely identifies the intent or semantics of the message. Windows Communication Foundation services rely on the action to dispatch incoming messages to the appropriate method. The message version identifies which versions of SOAP and WS-Addressing to use during transmission, if any. There are a variety of options to choose from when specifying the message version. Let's take a look at these options.
As mentioned, the MessageVersion class lets you specify the versions of SOAP and WS-Addressing you wish to use. You can create a MessageVersion object by calling CreateVersion and supplying an EnvelopeVersion object (to identify the SOAP version) along with an AddressingVersion object (to identify the WS-Addressing version). Here's what this looks like:
MessageVersion version = MessageVersion.CreateVersion( EnvelopeVersion.Soap12, AddressingVersion.WSAddressing10);
If you take a look at the EnvelopeVersion class, you'll see that Windows Communication Foundation currently supports three options for SOAP-None, Soap11, and Soap12, as shown here:
public sealed class EnvelopeVersion { public static EnvelopeVersion None { get; } public static EnvelopeVersion Soap11 { get; } public static EnvelopeVersion Soap12 { get; } ... }
Likewise, if you look at AddressingVersion, you'll see that Windows Communication Foundation currently supports three options for WS-Addressing-None, WSAddressing10, and WSAddressingAugust2004, as shown here:
public sealed class AddressingVersion { public static AddressingVersion None { get; } public static AddressingVersion WSAddressing10 { get; } public static AddressingVersion WSAddressingAugust2004 { get;} ... }
EnvelopeVersion.None specifies that you don't want to use SOAP during transmission, which also requires you to use AddressingVersion.None. This is a common setting for when you want to leverage Windows Communication Foundation in plain old XML messaging scenarios. Soap11 represents the SOAP 1.1 specification, which is in wide use today, and Soap12 represents the SOAP 1.2 W3C Recommendation (see www.w3.org/TR/soap for links to both specifications).
WSAddressingAugust2004 represents the original WS-Addressing W3C submission from August 2004 (see www.w3.org/Submission/ws-addressing), which is widely supported today. And WSAddressing10 represents the final WS-Addressing 1.0 W3C Recommendation (see www.w3.org/2002/ws/addr).
So, depending on which versions of SOAP and WS-Addressing the other side of the wire supports, you can pick and choose the right version to facilitate interoperability. And in order to make things a little easier for you, the MessageVersion class provides several public properties that return cached MessageVersion objects representing the most common combinations of both specifications (see Figure 6). So instead of calling CreateVersion explicitly, you can simply specify MessageVersion.Soap12WSAddressing10, which happens to be the same as MessageVersion.Default.
When you create a message from scratch, there are a great many overloads that let you specify the body of the message. You can supply the body as an XmlDictionaryReader, XmlReader, or serializable object. You can also supply a MessageFault object for the body in order to create a fault message. Once the message has been created, you can gain access to the body by calling GetReaderAtBodyContents, which returns an XmlReader, or by calling GetBody<T> to deserialize the body into a .NET object.
When you want to write a message, you can write either the entire message or just the body by calling the WriteMessage or WriteBody methods, respectively. Both have overloads that allow you to provide an XmlDictionaryWriter or XmlWriter object. The following example shows how to create a new message that's subsequently written to a file named message.xml:
// load body from customer.xml file XmlDocument doc = new XmlDocument(); doc.Load("customer.xml"); Message m = Message.CreateMessage( MessageVersion.Soap11WSAddressingAugust2004, "urn:add-customer", new XmlNodeReader(doc)); FileStream fs = new FileStream("message.xml", FileMode.Create); using (XmlWriter xw = XmlDictionaryWriter.CreateTextWriter(fs)) { m.WriteMessage(xw); }
In this case, "urn:add-customer" is specified for the action and Soap11WSAddressing2004 for the message version. The body is supplied via an XmlReader. And then WriteMessage is called to write the message out to a text-based XmlDictionaryWriter object. The resulting message.xml looks like this:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:add-customer</a:Action> </s:Header> <s:Body> <Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer> </s:Body> </s:Envelope>
Note that the XML namespaces indicate SOAP 1.1 and WS-Addressing from August 2004. It's important to point out that you could easily write the message out to a binary or MTOM representation using a different XmlDictionaryWriter object.
Now, if you wanted to, you could change the version to MessageVersion.Soap12WSAddressing10, and the resulting file would look like this instead:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:add-customer</a:Action> </s:Header> <s:Body> <Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer> </s:Body> </s:Envelope>
Notice the newer versions of SOAP and WS-Addressing.
And if you were to change the version to MessageVersion.None, the resulting message would no longer contain a SOAP envelope. Instead, it would simply contain the body:
<Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer>
In order to read the message back in, you need to use a different CreateMessage overload that allows you to provide an XmlDictionaryReader for reading the message stream. You also need to specify the correct message version. The code that follows shows an example that reads the last message above with no envelope (version is MessageVersion.None):
FileStream fs = new FileStream("message.xml", FileMode.Open); using (XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader( fs, XmlDictionaryReaderQuotas.Max)) { Message m = Message.CreateMessage(reader, 1024, MessageVersion.None); XmlDocument doc = new XmlDocument(); doc.Load(m.GetReaderAtBodyContents()); Console.WriteLine(doc.InnerXml); }
As you can see, the Message class provides great flexibility around reading and writing messages using different versions of SOAP and WS-Addressing-if using any of them at all. Also, the fact that it builds on XmlDictionaryReader for reading and writing operations means you can use any of the supported XML representations when transmitting messages.
So far, I've been showing examples of working directly with XML. When you'd rather deserialize XML into .NET objects, you can use one of the supported serializers, such as DataContractSerializer, NetDataContractSerializer, or XmlSerializer. The following class is capable of representing the XML found in customer.xml when used with DataContractSerializer:
[DataContract(Namespace="http://example.org/customer")] public class Customer { public Customer() { } public Customer(string name, string email) { this.Name = name; this.Email = email; } [DataMember] public string Name; [DataMember] public string Email; }
Now you can create messages by supplying Customer objects for the body, like this:
Customer cust = new Customer("Bob", "[email protected]"); Message msg = Message.CreateMessage( MessageVersion.Soap11WSAddressingAugust2004, "urn:add-customer", cust);
Despite using an object to represent the body, the resulting message will look identical to the previous examples.
Now, when reading the body, you can use GetBody<Customer> to produce a new Customer object, as shown here:
Customer c = msg.GetBody<Customer>(); Console.WriteLine("Name: {0}, Email: {1}", c.Name, c.Email);
These versions of CreateMessage and GetBody<T> use DataContractSerializer behind the scenes. There is another version of each method that allows you to explicitly specify the serializer you wish to use.
For more details, see my August 2006 column titled "Serialization in Windows Communication Foundation" (available at msdn.microsoft.com/msdnmag/issues/06/08/ServiceStation).
The Message class has been carefully designed to support streaming. As a consequence, the body can only be processed once during the lifetime of a Message object. In order to ensure this, the Message class provides a State property that describes the current state of the object. MessageState defines five possible message states:
public enum MessageState { Created, Read, Written, Copied, Closed }
A Message object starts out in the Created state, which is the only valid state for processing the body. There are a few different ways to process the body: you can read it, write it, or copy it. Calling either GetReaderAtBodyContents or GetBody<T> changes the state to Read. Calling either WriteMessage or WriteBody changes the state to Written. And calling CreateBufferedCopy changes the state to Copied. For example, consider the following example that moves a message through several states:
Customer cust = new Customer("Bob", "[email protected]"); Message m = Message.CreateMessage( MessageVersion.Soap11WSAddressingAugust2004, "urn:add-customer", cust); Console.WriteLine("State: {0}", m.State); Customer c = m.GetBody<Customer>(); // cannot access body from here on Console.WriteLine("State: {0}", m.State); m.Close(); Console.WriteLine("State: {0}", m.State);
When you execute the code, this text is printed to the console:
State: Created State: Read State: Closed
Once a Message object is no longer in the Created state, any method that needs access to the body will throw an exception. For example, calling GetBody<T> again, after the first call results in an exception. In situations where you need to process the body multiple times, you can create a buffered copy (by calling CreateBufferedCopy) or you can load the body into an XmlDocument or deserialize it into a .NET object for in-memory use.
Needless to say, the State property makes it possible to determine at runtime if the body of a particular Message has already been consumed (when it's no longer in the Created state) and how it was consumed (whether it was read, written, or copied).
Message Headers and Properties
The Message class provides a Headers property to model the collection of headers associated with the body. The following example illustrates how to add a custom header named "ContextId" to a message:
Customer cust = new Customer("Bob", "[email protected]"); Message m = Message.CreateMessage( MessageVersion.Default, "urn:add-customer", cust); m.Headers.Add( MessageHeader.CreateHeader( "ContextId", "http://example.org/customHeaders", Guid.NewGuid()));
When this message is written, it looks like this:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:add-customer</a:Action> <ContextId xmlns="http://example.org/customHeaders" >a200f76d-5b83-4496-a035-4f9e70a07959</ContextId> </s:Header> ... </s:Envelope>
It's typically the job of intermediaries to process headers found in a message. Such intermediaries often need to store the results of their processing somewhere for future use in the processing pipeline. For this purpose, the Message class also provides a Properties collection for storing arbitrary name/object pairs.
Unlike headers, which are always transmitted in the message, message properties are typically only used during local processing-they may not have any impact on what happens on the wire, although sometimes they do. For example, when using HTTP, Windows Communication Foundation stores the HTTP request details in the Properties collection of the incoming Message object. The details are stored in an object of type HttpRequestMessageProperty, which you can access using httpRequest for the property name. You can also influence the HTTP response details by populating a property named httpResponse with an object of type HttpReponseMessageProperty.
It's common for components within the Windows Communication Foundation channel layer to make heavy use of headers and properties-especially the built-in protocol channels that implement the various WS-* specifications.
Before you can begin sending or receiving messages with Windows Communication Foundation, you must first define a service contract that maps incoming messages to methods. This is accomplished by associating action values with method signatures. For example, the following interface defines the most universal service contract possible for a one-way operation:
[ServiceContract] public interface IUniversalOneWay { [OperationContract(Action="*")] void ProcessMessage(Message msg); }
This definition associates ProcessMessage with all incoming messages regardless of their action values (thanks to the Action= "*" clause). It's also possible to define a generic request-reply operation using the same technique:
[ServiceContract] public interface IUniversalRequestReply { [OperationContract(Action="*", ReplyAction="*")] Message ProcessMessage(Message msg); }
You can also associate methods with specific action values by setting the Action property to a specific value like urn:add-customer, as shown here:
[ServiceContract] public interface ICustomer { [OperationContract(Action="urn:add-customer")] void AddCustomer(Message msg); }
When Windows Communication Foundation receives an incoming message, it looks at the action to determine which method to dispatch to and then it performs any necessary serialization. When using Message for the request/response types, Windows Communication Foundation doesn't perform any serialization-you decide how to process the message. However, when using typed signatures, Windows Communication Foundation automates the process of reading and writing the message bodies behind the scenes using serialization techniques. For example, consider this service contract:
[ServiceContract] public interface ICustomer { [OperationContract(Action="urn:add-customer")] void AddCustomer(Customer cust); }
In this case, Windows Communication Foundation takes care of deserializing the body of the incoming message into a Customer object before invoking AddCustomer.
There is another serialization shortcut, known as message contracts, for automatically adding headers to a Message object. Message contracts allow you to annotate a class, specifying which fields map to headers versus the body:
[MessageContract(IsWrapped=false)] public class AddCustomerRequest { [MessageHeader(Name="ContextId", Namespace="http://example.org/customHeaders"] public Guid ContextId; [MessageBodyMember] public Customer customer; }
With this message contract class in place, you can define an operation that uses it as the request type, as shown here:
[ServiceContract] public interface ICustomer { [OperationContract(Action = "urn:add-customer")] void AddCustomer(AddCustomerRequest request); }
This tells Windows Communication Foundation to automatically map between the ContextId field in the object and the ContextId header in the SOAP message, freeing you from having to manually deal with the Headers collection.
In order to wire-up a Windows Communication Foundation service, you need to provide several pieces of information. First, you must tell it what transport and address to use. Second, you must specify what XML representation and message version to expect. Third, you must configure which WS-* protocols to wire-in. And finally, you must provide the mapping between the incoming messages and the service methods. In Windows Communication Foundation, you specify all of these details via endpoints.
An endpoint configuration is simply a combination of an address, binding, and contract. As I already discussed, the contract defines the mapping between messages and methods. The binding specifies the remaining messaging details. It specifies what transport, XML representation, and message version to use during transmission. It also identifies which WS-* protocols should be included when building the channel stack.
A binding specifies a message encoder; this is what actually controls the XML representation and message version details. At run time, the message encoder is the component used by the transport channel to read and write messages to and from a stream. Windows Communication Foundation provides three built-in message encoder implementations: TextMessageEncoder, BinaryMessageEncoder, and MtomMessageEncoder. All of these classes build on the new XmlDictionaryReader and XmlDictionaryWriter classes discussed earlier to support a given XML representation. Each class also comes configured to use a specific message version.
As you may know, Windows Communication Foundation comes with built-in bindings to facilitate common scenarios. Each binding is configured to use a particular message encoder. For example, both BasicHttpBinding and WSHttpBinding are configured to use the TextMessageEncoder. However, the former uses MessageVersion.Soap11 while the latter uses MessageVersion.Soap12WSAddressing10 for the message version. NetTcpBinding, NetNamedPipeBinding, and NetMsmqBinding all come configured to use the BinaryMessageEncoder and MessageVersion.Soap12WSAddressing10.
You can opt to use a different message encoder on a particular binding through its properties or a binding configuration. For example, the following shows how to change BasicHttpBinding's message encoder to MtomMessageEncoder:
BasicHttpBinding basic = new BasicHttpBinding(); basic.MessageEncoding = WSMessageEncoding.Mtom; ... // use binding
When you need more control over the binding configuration, you can define a custom binding using the CustomBinding class:
MtomMessageEncodingBindingElement mtom = new MtomMessageEncodingBindingElement( MessageVersion.Soap12, Encoding.UTF8); CustomBinding binding = new CustomBinding(); binding.Elements.Add(mtom); binding.Elements.Add(new HttpTransportBindingElement()); ... // use binding
This technique allows you to configure the precise message version and text encoding details used by the message encoder. In addition to this flexibility, the Windows Communication Foundation architecture lets you write and use custom message encoders-the SDK comes with a sample that shows how to do this. In general, Windows Communication Foundation makes it easy to configure endpoints to use different XML representations and message versions without having any impact on your service code.
Let's look at a final example that brings most of these messaging concepts together. Figure 7 shows how to implement, host, and configure a generic service that uses the IUniversalOneWay contract along with a customized binding and encoder. Figure 8 shows how to implement a client capable of sending messages to the hosted service. The service implementation uses System.Xml to process the incoming message while the client produces a message using a Customer object.
The Windows Communication Foundation messaging architecture provides a unified programming model that offers significant messaging flexibility. This makes it possible to seamlessly implement plain-old XML services that don't use SOAP, WS-Addressing, or any other WS-* protocol. It also makes it possible to configure the more sophisticated bindings to meet the precise needs of specific integration scenarios when different protocols and versions are required.
Send your questions and comments for Aaron to [email protected].
NEW: Explore the sample code online! - or - Code download available at: ServiceStation2007_04.exe (161KB)
Aaron Skonnard is a cofounder of Pluralsight, a Microsoft .NET training provider. Aaron is the author of Pluralsight's Applied Web Services 2.0, Applied BizTalk Server 2006, and Introducing Windows Communication Foundation courses. Aaron has spent years developing courses, speaking at conferences, and teaching professional developers. Reach him at pluralsight.com/aaron.
From the April 2007 issue of MSDN Magazine.
Source: Service Station: WCF Messaging Fundamentals -- MSDN Magazine, April 2007
WCF 消息传递基础
Explore the code.
Download the code.
Get the sample code for this article.
NEW: Explore the sample code online!
- or -
代码下载位置: ServiceStation2007_04.exe (161KB)
XML 表示形式
Message 类
消息版本
读写消息
键入的消息正文
消息生存期
消息标头和属性
将消息映射到方法
端点和绑定
综述
开
始分离 Windows® Communication Foundation 的各层时,您会发现一种复杂的基于 XML 的消息传递框架,它在使用各种协议和格式连接系统时提供了大量的可能性。在本月的专栏中,我将着重介绍一些主要的消息传递功能,它们提供了这样的灵活性。
本专栏假定您对 Windows Communication Foundation 编程模型具有基本的了解。如果您不熟悉它,那么在继续之前,您应该阅读 2006 年 2 月份的《MSDN® 杂志》中我的文章。
Windows Communication Foundation 消息传递体系结构的主要目的之一是,在提供统一编程模型的同时,还允许灵活地表示数据和传递消息。这是基于将 XML 作为数据模型以及将 SOAP 和 WS-Addressing 作为消息传递框架而实现的。但是,Windows Communication Foundation 构建在这些模型基础上这一事实,并不意味着在传递消息时必须使用 XML 1.0、SOAP 或 WS-Addressing。您将会看到 Windows Communication Foundation 提供了很大的灵活性。
从早期的 XML 开始,软件行业就依赖于为在 XML 文档中找到的数据提供标准定义的鲜为人知的规范。此规范称为 XML 信息集 (InfoSet),它根据元素和属性所包含的信息来定义它们,在某种意义上这完全与字节表示形式无关。(有关详细信息,请参阅 www.w3.org/TR/xml-infoset。)
InfoSet 规范使得其他 XML 规范和 API 为在 XML 文档中找到的数据提供一致的视图成为可能(尽管它们可能以完全不同的方式表示该数据)。最终,InfoSet 为使用 XML 数据的应用程序提供了共同的集合点,如图 1 所示。最后要说的是,XML 处理器负责在字节表示形式和编程模型体验之间的转换。
图 1 XML InfoSet 的角色 (单击该图像获得较小视图)
图 1 XML InfoSet 的角色 (单击该图像获得较大视图)
Windows Communication Foundation 为 System.Xml 命名空间引入了一些基本增强,这使得在读写 XML 文档时利用替代的字节表示形式(而不是仅限文本的 XML 1.0)成为可能。此处关注的主要类 System.Xml.XmlDictionaryReader 和 System.Xml.XmlDictionaryWriter 位于 Microsoft® .NET Framework 3.0 附带的新 System.Runtime.Serialization 程序集中。
XmlDictionaryReader 和 XmlDictionaryWriter 类都提供了静态工厂方法,用于创建使用文本、二进制和 MTOM(消息传输优化机制)表示形式的读取器和编写器。例如,XmlDictionaryReader 提供了 CreateTextReader、CreateBinaryReader 和 CreateMtomReader 方法,而 XmlDictionaryWriter 提供了对应的 CreateTextWriter、CreateBinaryWriter 和 CreateMtomWriter 方法。
让我们看一些示例。试考虑某个客户的以下文本 XML 1.0 表示形式:
<!-- customer.xml --> <Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer>
以下代码说明如何将 customer.xml 读入 XmlDocument 对象,以及如何使用 XmlDictionaryWriter 以二进制表示形式重新将同一 XmlDocument 对象保存在磁盘上:
// read from XML 1.0 text representation XmlDocument doc = new XmlDocument(); doc.Load("customer.xml"); // write to binary representation FileStream custBinStream = new FileStream( "customer.bin", FileMode.Create); using (XmlWriter xw = XmlDictionaryWriter.CreateBinaryWriter( custBinStream)) { doc.WriteContentTo(xw); }
运行此代码后,您就将二进制表示形式保存在 customer.bin 中,如图 2 所示。即使图 2 所示的二进制编辑器碰巧显示字符序列,这实际上也是二进制文件,显然不像 XML 1.0 文件。不过,它确实表示 XML 文档 (InfoSet)。尽管如此,此二进制格式是 3.0 版 System.Xml 专有的,它与其他 .NET 版本或 Web 服务框架不兼容。
图 2 客户 XML 文档的二进制表示形式 (单击该图像获得较小视图)
图 2 客户 XML 文档的二进制表示形式 (单击该图像获得较大视图)
图 3 中的代码示例演示如何将二进制表示形式读回到 XmlDocument 对象中,以及如何以 MTOM 表示形式将它重新保存到磁盘上。MTOM 表示形式看起来与文本或二进制表示形式差别很大,如图 4 所示。不过,它表示同一 XML 文档,并可以使用相同的 System.Xml API 进行处理。MTOM 表示形式也与该 MTOM 中的二进制表示形式不同,因为 MTOM 是基于 InfoSet 的 W3C 推荐标准,在 Web 服务框架之间得到广泛的支持。通过利用多部分 MIME 组帧,MTOM 使得优化 XML 文档中二进制元素的传输成为可能。
图 4 客户 XML 文档的 MTOM 表示形式 (单击该图像获得较小视图)
图 4 客户 XML 文档的 MTOM 表示形式 (单击该图像获得较大视图)
现在,通过将 MTOM 表示形式再读入 XmlDocument 并以文本表示形式将其保存在磁盘上,我们可以执行一个完整循环,如下所示:
// read from MTOM representation XmlDocument doc = new XmlDocument(); FileStream custMtomStream = new FileStream( "customer.mtom", FileMode.Open); using (XmlReader xr = XmlDictionaryReader.CreateMtomReader( custMtomStream, Encoding.UTF8, XmlDictionaryReaderQuotas.Max)) { doc.Load(xr); } // write to text (XML 1.0) representation doc.Save("customer.xml");
生成的 custom.xml 文件应该与原始的文本表示形式完全相同。正如您所看到的,通过提供三种方法来表示 XML 以实现持久性和传输,这些 System.Xml 增强功能提供了很大的灵活性。当然,将来可能会添加其他表示形式。此外,虽然是 Windows Communication Foundation 为表带来了这些增强,但是可以在任何 .NET Framework 3.0 应用程序中利用它们。
您可以选择在传递消息时使用的 XML 表示形式,Windows Communication Foundation 即基于此而构建。如果需要互操作性,则应该选择 XML 1.0 文本表示形式。如果需要互操作性以及对二进制负载的高效支持,则应该选择 MTOM 表示形式。在只有 .NET 的方案中,二进制表示形式可能会提供更佳的性能。此处的关键是您确实可以进行选择了。
任何消息传递框架的另一个主要功能是,通过任意标头扩展消息负载。标头不过是随消息传递的额外信息,用于实现其他的消息处理功能(如安全性、可靠的消息传递和事务)。对于 XML 消息,这意味着用 XML 标头扩展 XML 负载(两者都表示为在容器元素中分帧的 XML 元素)。此功能与 SOAP 提供的完全相同。
SOAP 框架使得定义基于 XML 的协议(可通过任何传输使用,而不依赖于任何传输特定的功能)成为可能。WS-Addressing 是一种规范,用于扩展 SOAP 以提供与传输无关的机制来寻址/路由 SOAP 消息。SOAP 和 WS-Addressing 都基于 InfoSet,且允许(但不要求)在传递消息时使用 XML 1.0 语法。
Windows Communication Foundation 支持使用上一部分中讨论的任何 XmlDictionaryReader/Writer 表示形式,这样应用程序就可以无缝地满足各种范围和性能要求。Windows Communication Foundation 使用图 5 所示的 Message 类模拟所有消息。正如您可以看到的,Message 类在本质上模拟消息正文以及消息标头和属性的集合。可用方法主要用于创建消息、读写消息正文以及操作标头和属性的集合。
通过调用各种静态 CreateMessage 重载之一创建 Message 对象,并使用 IDisposable 或通过显式调用 Close 处理 Message 对象。可以从头开始创建新的 Message 对象,在发送消息时通常这样做。也可以从消息流创建新的 Message 对象,在接收消息时通常这样做。
如果从头开始创建消息,则必须指定操作、消息版本以及要在消息中使用的正文。操作唯一地标识消息的目的或语义。Windows Communication Foundation 服务依赖于将传入消息分派给相应方法的操作。消息版本标识传输时使用的 SOAP 和 WS-Addressing 版本(如果有)。指定消息版本时可以选择各种不同的选项。让我们了解一下这些选项。
如上所述,通过 MessageVersion 类,可以指定要使用的 SOAP 和 WS-Addressing 版本。通过调用 CreateVersion 并提供 EnvelopeVersion 对象(用于标识 SOAP 版本)和 AddressingVersion 对象(用于标识 WS-Addressing 版本),可以创建 MessageVersion 对象。如下所示:
MessageVersion version = MessageVersion.CreateVersion( EnvelopeVersion.Soap12, AddressingVersion.WSAddressing10);
如果查看一下 EnvelopeVersion 类,则将看到 Windows Communication Foundation 当前支持三个选项(即 SOAP-None、Soap11 和 Soap12),如下所示:
public sealed class EnvelopeVersion { public static EnvelopeVersion None { get; } public static EnvelopeVersion Soap11 { get; } public static EnvelopeVersion Soap12 { get; } ... }
同样,如果查看一下 AddressingVersion,则将看到 Windows Communication Foundation 当前也支持三个选项(即 WS-Addressing-None、WSAddressing10 和 WSAddressingAugust2004),如下所示:
public sealed class AddressingVersion { public static AddressingVersion None { get; } public static AddressingVersion WSAddressing10 { get; } public static AddressingVersion WSAddressingAugust2004 { get;} ... }
EnvelopeVersion.None 指定在传输过程中您不希望使用 SOAP,它还要求您使用 AddressingVersion.None。这是希望在传统的 XML 消息传递方案中利用 Windows Communication Foundation 时的常见设置。Soap11 表示 SOAP 1.1 规范,该规范目前得到广泛使用,而 Soap12 表示 SOAP 1.2 W3C 推荐标准(有关指向这两种规范的链接,请参阅 www.w3.org/TR/soap)。
WSAddressingAugust2004 表示 2004 年 8 月开始提交的 WS-Addressing W3C 提案(请参阅 www.w3.org/Submission/ws-addressing),目前得到广泛支持。而 WSAddressing10 表示最终的 WS-Addressing 1.0 W3C 推荐标准(请参阅 www.w3.org/2002/ws/addr)。
因此,根据进行通信的另一方支持的 SOAP 和 WS-Addressing 版本,可以仔细挑选合适的版本以便利互操作性。此外,为了使您操作起来更轻松一点,MessageVersion 类提供了几个公共属性,这些属性返回表示这两种规范的最常见组合的已缓存 MessageVersion 对象(参见图 6)。因此,只需指定 MessageVersion.Soap12WSAddressing10(其作用与 MessageVersion.Default 相同),而不是显式调用 CreateVersion。
从头开始创建消息时,可以使用许多重载来指定消息的正文。可以将正文作为 XmlDictionaryReader、XmlReader 或可序列化的对象提供。还可以为正文提供 MessageFault 对象以便创建故障消息。创建消息后,可以通过调用返回 XmlReader 的 GetReaderAtBodyContents 或调用 GetBody<T> 将正文反序列化为 .NET 对象来访问正文。
在希望写入消息时,可以通过分别调用 WriteMessage 或 WriteBody 方法写入整个消息或仅写入正文。这两种方法都具有允许提供 XmlDictionaryWriter 或 XmlWriter 对象的重载。以下示例说明如何创建随后写入到名为 message.xml 的文件中的新消息:
// load body from customer.xml file XmlDocument doc = new XmlDocument(); doc.Load("customer.xml"); Message m = Message.CreateMessage( MessageVersion.Soap11WSAddressingAugust2004, "urn:add-customer", new XmlNodeReader(doc)); FileStream fs = new FileStream("message.xml", FileMode.Create); using (XmlWriter xw = XmlDictionaryWriter.CreateTextWriter(fs)) { m.WriteMessage(xw); }
在这种情况下,为操作指定了“urn:add-customer”,为消息版本指定了 Soap11WSAddressing2004。正文是通过 XmlReader 提供的。然后调用 WriteMessage 将消息写出到基于文本的 XmlDictionaryWriter 对象。生成的 message.xml 如下所示:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:add-customer</a:Action> </s:Header> <s:Body> <Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer> </s:Body> </s:Envelope>
请注意,XML 命名空间指示 SOAP 1.1 和 2004 年 8 月的 WS-Addressing。指出可以使用不同的 XmlDictionaryWriter 对象将消息轻松地写出到二进制或 MTOM 表示形式是很重要的。
现在,如果希望,则可以将版本更改为 MessageVersion.Soap12WSAddressing10,而生成的文件则将如下所示:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:add-customer</a:Action> </s:Header> <s:Body> <Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer> </s:Body> </s:Envelope>
请注意 SOAP 和 WS-Addressing 的较新版本。
如果将版本更改为 MessageVersion.None,则生成的消息将不再包含 SOAP 信封。相反,它将仅包含正文:
<Customer xmlns="http://example.org/customer"> <Email>[email protected]</Email> <Name>Bob</Name> </Customer>
为了读回消息,需要使用允许您提供用于读取消息流的 XmlDictionaryReader 的不同 CreateMessage 重载。您还需要指定正确的消息版本。接下来的代码说明一个读取上面无信封的最后一条消息的示例(版本为 MessageVersion.None):
FileStream fs = new FileStream("message.xml", FileMode.Open); using (XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader( fs, XmlDictionaryReaderQuotas.Max)) { Message m = Message.CreateMessage(reader, 1024, MessageVersion.None); XmlDocument doc = new XmlDocument(); doc.Load(m.GetReaderAtBodyContents()); Console.WriteLine(doc.InnerXml); }
正如您可以看到的,Message 类为使用不同版本的 SOAP 和 WS-Addressing(如果使用了它们中的任一个)读写消息提供了很大的灵活性。此外,它基于 XmlDictionaryReader 执行读写操作的这一事实意味着,在传递消息时可以使用所有支持的 XML 表示形式。
到目前为止,我介绍了直接使用 XML 的示例。如果要将 XML 反序列化为 .NET 对象,则可以使用支持的序列化程序之一,如 DataContractSerializer、NetDataContractSerializer 或 XmlSerializer。在与 DataContractSerializer 一起使用时,以下类能够表示在 customer.xml 中找到的 XML:
[DataContract(Namespace="http://example.org/customer")] public class Customer { public Customer() { } public Customer(string name, string email) { this.Name = name; this.Email = email; } [DataMember] public string Name; [DataMember] public string Email; }
现在,可以通过为正文提供 Customer 对象创建消息,如下所示:
Customer cust = new Customer("Bob", "[email protected]"); Message msg = Message.CreateMessage( MessageVersion.Soap11WSAddressingAugust2004, "urn:add-customer", cust);
尽管使用一个对象来表示正文,但是生成的消息将与前面的示例完全相同。
现在,读取正文时,可以使用 GetBody<Customer> 生成新的 Customer 对象,如下所示:
Customer c = msg.GetBody<Customer>(); Console.WriteLine("Name: {0}, Email: {1}", c.Name, c.Email);
这些版本的 CreateMessage 和 GetBody<T> 在后台使用 DataContractSerializer。每个方法还有一个版本,允许您显式指定要使用的序列化程序。
有关详细信息,请参阅我在 2006 年 8 月撰写的题为“在 Windows Communication Foundation 中进行序列化”专栏(可以从 msdn.microsoft.com/msdnmag/issues/06/08/ServiceStation 下载)。
经过仔细地设计,Message 类已支持流处理。因此,在 Message 对象的生存期内只能处理一次正文。为了确保这一点,Message 类提供了描述对象当前状态的 State 属性。MessageState 定义以下五种可能的消息状态:
public enum MessageState { Created, Read, Written, Copied, Closed }
Message 对象在开始时处于 Created 状态,该状态是处理正文的唯一有效状态。处理正文有以下几种不同的方式:可以对其进行读取、写入或复制。调用 GetReaderAtBodyContents 或 GetBody<T> 可将状态更改为 Read。调用 WriteMessage 或 WriteBody 可将状态更改为 Written。调用 CreateBufferedCopy 可将状态更改为 Copied。例如,试考虑以下使消息在几种状态之间切换的示例:
Customer cust = new Customer("Bob", "[email protected]"); Message m = Message.CreateMessage( MessageVersion.Soap11WSAddressingAugust2004, "urn:add-customer", cust); Console.WriteLine("State: {0}", m.State); Customer c = m.GetBody<Customer>(); // cannot access body from here on Console.WriteLine("State: {0}", m.State); m.Close(); Console.WriteLine("State: {0}", m.State);
执行代码时,会将此文本打印到控制台:
State: Created State: Read State: Closed
在 Message 对象不再处于 Created 状态后,需要访问正文的任何方法都将引发异常。例如,首次调用 GetBody<T> 后再次调用它将导致异常。在需要多次处理正文的情况下,可以创建缓冲副本(通过调用 CreateBufferedCopy)或者将正文加载到 XmlDocument 中或将它反序列化为 .NET 对象以便在内存中使用。
不用说,通过 State 属性可以在运行时确定是否已使用特定消息的正文(它不再处于 Created 状态时)以及如何使用它(它是被读取、写入还是复制)。
Message 类提供了 Headers 属性,以模拟与正文关联的标头集合。以下示例说明如何将名为“ContextId”的自定义标头添加到消息中:
Customer cust = new Customer("Bob", "[email protected]"); Message m = Message.CreateMessage( MessageVersion.Default, "urn:add-customer", cust); m.Headers.Add( MessageHeader.CreateHeader( "ContextId", "http://example.org/customHeaders", Guid.NewGuid()));
在写入时,此消息将如下所示:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:add-customer</a:Action> <ContextId xmlns="http://example.org/customHeaders" >a200f76d-5b83-4496-a035-4f9e70a07959</ContextId> </s:Header> ... </s:Envelope>
通常由中介来处理在消息中找到的标头。这样的中介通常需要在某处存储其处理结果,供将来在处理管道时使用。为达到此目的,Message 类还提供了 Properties 集合来存储任意的名称/对象对。
与标头(它始终在消息中传输)不同,消息属性通常仅在本地处理期间使用(它们可能不对通信时发生的情况产生影响,尽管有时它们会产生影响)。例如,使用 HTTP 时,Windows Communication Foundation 在传入 Message 对象的 Properties 集合中存储 HTTP 请求详细信息。详细信息存储在 HttpRequestMessageProperty 类型的对象(可以使用属性名的 httpRequest 访问它)中。还可以通过用 HttpReponseMessageProperty 类型的对象填充名为 httpResponse 的属性,影响 HTTP 响应详细信息。
Windows Communication Foundation 通道(尤其是实现各种 WS-* 规范的内置协议通道)层中的组件频繁使用标头和属性是很常见的。
必须首先定义将传入消息映射到方法的服务约定,才能开始使用 Windows Communication Foundation 发送或接收消息。通过使操作值与方法签名关联,可以做到这一点。例如,以下接口为单向操作定义可能的最通用服务约定:
[ServiceContract] public interface IUniversalOneWay { [OperationContract(Action="*")] void ProcessMessage(Message msg); }
此定义使 ProcessMessage 与所有传入消息关联,而不管其操作值如何(由于使用了 Action= "*" 子句)。也可以使用相同方法定义一般的请求-答复操作:
[ServiceContract] public interface IUniversalRequestReply { [OperationContract(Action="*", ReplyAction="*")] Message ProcessMessage(Message msg); }
还可以通过将 Action 属性设置为特定值(如 urn:add-customer)使方法与特定操作值关联,如下所示:
[ServiceContract] public interface ICustomer { [OperationContract(Action="urn:add-customer")] void AddCustomer(Message msg); }
在 Windows Communication Foundation 收到传入消息时,它将查看操作以确定要调度的方法,然后它再执行任何必要的序列化。对请求/响应类型使用消息时,Windows Communication Foundation 不执行任何序列化,而是由您决定如何处理消息。但是,使用键入的签名时,Windows Communication Foundation 使用序列化方法在后台自动执行读写消息正文的过程。例如,试考虑以下服务约定:
[ServiceContract] public interface ICustomer { [OperationContract(Action="urn:add-customer")] void AddCustomer(Customer cust); }
在这种情况下,Windows Communication Foundation 负责在调用 AddCustomer 之前将传入消息的正文反序列化为 Customer 对象。
还存在一种称为消息约定的序列化快捷方式,用于自动将标头添加到 Message 对象。消息约定允许您为类添加注释,指定哪些字段映射到标头(与正文相对):
[MessageContract(IsWrapped=false)] public class AddCustomerRequest { [MessageHeader(Name="ContextId", Namespace="http://example.org/customHeaders"] public Guid ContextId; [MessageBodyMember] public Customer customer; }
如果此消息约定类已就绪,则可以定义一个将它用作请求类型的操作,如下所示:
[ServiceContract] public interface ICustomer { [OperationContract(Action = "urn:add-customer")] void AddCustomer(AddCustomerRequest request); }
这样会通知 Windows Communication Foundation 在对象中的 ContextId 字段和 SOAP 消息中的 ContextId 标头之间自动映射,您就不必手动处理 Headers 集合了。
为了布置 Windows Communication Foundation 服务,您需要提供几则信息。首先,必须通知它要使用的传输和地址。第二,必须指定所需的 XML 表示形式和消息版本。第三,必须配置采用哪些 WS-* 协议。最后,必须提供传入消息和服务方法之间的映射。在 Windows Communication Foundation 中,通过端点指定所有这些详细信息。
端点配置仅仅是地址、绑定和约定的组合。如上所述,约定定义消息和方法之间的映射。绑定指定剩余的消息传递详细信息。它指定在传递期间要使用的传输、XML 表示形式和消息版本。它还确定生成通道堆栈时应该包括的 WS-* 协议。
绑定还指定消息编码器;它实际上控制 XML 表示形式和消息版本详细信息。在运行时,消息编码器是传输通道从数据流读取和向其写入消息所用的组件。Windows Communication Foundation 提供了三个内置的消息编码器实现:TextMessageEncoder、BinaryMessageEncoder 和 MtomMessageEncoder。所有这些类都基于在前面讨论的新增 XmlDictionaryReader 和 XmlDictionaryWriter 类支持给定的 XML 表示形式。每个类还配置为使用特定的消息版本。
正如您可能知道的,Windows Communication Foundation 附带有内置绑定以便于处理常见方案。每个绑定都配置为使用特定的消息编码器。例如,BasicHttpBinding 和 WSHttpBinding 都配置为使用 TextMessageEncoder。但是,对于消息版本,前者使用 MessageVersion.Soap11,而后者使用 MessageVersion.Soap12WSAddressing10。NetTcpBinding、NetNamedPipeBinding 和 NetMsmqBinding 都配置为使用 BinaryMessageEncoder 和 MessageVersion.Soap12WSAddressing10。
可以选择在特定的绑定上使用不同的消息编码器(通过其属性或绑定配置)。例如,以下代码说明如何将 BasicHttpBinding 的消息编码器更改为 MtomMessageEncoder:
BasicHttpBinding basic = new BasicHttpBinding(); basic.MessageEncoding = WSMessageEncoding.Mtom; ... // use binding
在需要对绑定配置进行更多控制时,可以使用 CustomBinding 类定义自定义绑定:
MtomMessageEncodingBindingElement mtom = new MtomMessageEncodingBindingElement( MessageVersion.Soap12, Encoding.UTF8); CustomBinding binding = new CustomBinding(); binding.Elements.Add(mtom); binding.Elements.Add(new HttpTransportBindingElement()); ... // use binding
使用此方法可以配置消息编码器使用的精确消息版本和文本编码详细信息。除了此灵活性外,Windows Communication Foundation 体系结构还允许您编写和使用自定义消息编码器 - SDK 附带有一个说明如何执行此操作的示例。通常,使用 Windows Communication Foundation 可以轻松地将端点配置为使用不同的 XML 表示形式和消息版本,而不会对服务代码产生任何影响。
让我们看一下最后一个示例,该示例综合了其中的大多数消息传递概念。图 7 说明如何实现、承载和配置使用 IUniversalOneWay 约定以及自定义的绑定和编码器的通用服务。图 8 说明如何实现能够将消息发送到所承载的服务的客户端。服务实现使用 System.Xml 处理传入消息,而客户端使用 Customer 对象生成消息。
Windows Communication Foundation 消息传递体系结构提供了统一的编程模型,从而为消息传递提供了很大的灵活性。这样就可以无缝地实现不使用 SOAP、WS-Addressing 或任何其他 WS-* 协议的传统的 XML 服务。还可以配置更复杂的绑定以满足在需要不同的协议和版本时特定集成方案的精确要求。
将您想向 Aaron 提出的问题和意见发送至:[email protected] [email protected].
NEW: Explore the sample code online! - or - 代码下载位置: ServiceStation2007_04.exe (161KB)
Aaron Skonnard 是 Microsoft .NET 培训提供商 Pluralsight 的创始人之一。Aaron 是 Pluralsight 推出的“Web Services 2.0 应用”(Applied Web Services 2.0)、“BizTalk Server 2006 应用”(Applied BizTalk Server 2006) 和“Windows Communication Foundation 入门”(Introducing Windows Communication Foundation) 等众多课程的作者。多年来,Aaron 一直致力于编写课程、会议演讲以及面向专业开发人员传授知识。您可以通过 pluralsight.com/aaron 与他联系。
摘自 April 2007 期刊 MSDN Magazine.