消息交換是WCF進行通信的唯一手段,通過方法調用(Method Call)形式體現的服務訪問需要轉化成具體的消息,並通過相應的編碼(Encoding)才能通過傳輸通道發送到服務端;服務操作執行的結果也只能以消息的形式才能被正常地返回到客戶端。所以,消息在整個WCF體系結構中處於一個核心的地位,WCF可以看成是一個消息處理的管道。
盡管消息在整個WCF體系中具有如此重要的意義,可是一般的WCF編程人員,卻意識不到消息的存在。原因很簡單,WCF設計的目標就是實現消息通信的所有細節,為最終的編程人員提供一個完全面向對象的編程模型。所以對於一般的編程人員來說,他們面對的是接口,卻不知道服務契約對於服務的描述;面對的是數據類型,卻不知道數據契約對序列化的作用;面對的是方法調用和返回值的獲取,卻不了解底層消息交換的過程。
鼓勵大家深入了解WCF關於消息處理的流程具有兩個目的:第一,只有在對整個消息處理流程具有清晰認識的基礎上才能寫出高質量的WCF程序。第二,WCF是一個極具可擴展性的通信框架,可以靈活地創建一些自定義WCF擴展(WCF Extension)以實現你所需要的功能。如同WCF的插件一樣,這些自定義的WCF擴展以即插即用的方式參與到WCF整個消息處理流程之中。了解WCF整個消息處理流程是靈活進行WCF擴展的前提。
在WCF中,定義了一個System.ServiceModel.Channels.Message類,用以表示這些具有不同表現形態的消息。在本篇文章中,我們會著重來討論這個Message類型。首先來介紹消息的版本。
一、消息版本(Message Version)
由於消息基於不同的格式或者結構,不同的格式決定了對消息不同的處理方式,所以對一個消息進行正確處理的前提是確定消息的格式或結構。在WCF中消息的格式與結構由消息的版本決定,在Message中定義了一個類型為MessageVersion的Version屬性來表示消息的版本。
1: public abstract class Message : IDisposable
2: {
3: //其他成員
4: public abstract MessageVersion Version { get; }
5: }
MessageVersion類型定義在System.ServiceModel.Channels命名空間下。由於SOAP規范的版本和WS-Addressing規范的版本是決定消息格式與結構的兩個主要因素,所以,MessageVersion由SOAP規范和WS-Addressing規范共同決定。WCF通過System.ServiceModel.EnvelopeVersion和System.ServiceModel.AddressingVersion兩個類分別定義SOAP規范的版本和WS-Addressing的版本。
MessageVersion中定義兩個靜態的方法CreateVersion用以創建相應的MessageVersion對象,兩個屬性Envelope和Addressing分別表示通過EnvelopeVersion和AddressingVersion體現的SOAP規范版本和WS-Addressing規范版本。
1: public sealed class MessageVersion
2: {
3: //其他成員
4: public static MessageVersion CreateVersion(EnvelopeVersion envelopeVersion);
5: public static MessageVersion CreateVersion(EnvelopeVersion envelopeVersion, AddressingVersion addressingVersion);
6:
7: public AddressingVersion Addressing { get; }
8: public EnvelopeVersion Envelope { get; }
9: }
到目前為止SOAP和WS-Addressing各有兩個版本:SOAP 1.1 和SOAP1.2, WS-Addressing 2004和WS-Addressing 1.0。它們分別通過定義在EnvelopeVersion和AddressingVersion中相應的靜態只讀屬性表示。Soap11和Soap12代表SOAP 1.1和SOAP1.2,而WSAddressingAugust2004和WSAddressing10則表示WS-Addressing 2004和WS-Addressing 1.0。EnvelopeVersion.None表示消息並非一個SOAP消息,比如非XML結構的消息(比如基於JSON格式)以及POX(Plain Old XML)消息。AddressingVersion.None則表示消息不遵循WS-Addressing規范,比如通過手工方式解決尋址問題。
1: public sealed class EnvelopeVersion
2: {
3: //其他成員
4: public static EnvelopeVersion None { get; }
5: public static EnvelopeVersion Soap11 { get; }
6: public static EnvelopeVersion Soap12 { get; }
7: }
1: public sealed class AddressingVersion
2: {
3: //其他成員
4: public static AddressingVersion None { get; }
5: public static AddressingVersion WSAddressing10 { get; }
6: public static AddressingVersion WSAddressingAugust2004 { get; }
7: }
注: MessageVersion的靜態方法CreateVersion(EnvelopeVersion envelopeVersion)默認采用的AddressingVersion為WSAddressing10。
由於EnvelopeVersion和AddressingVersion共同決定了MessageVesion。所以EnvelopeVersion和AddressingVersion的兩兩組合就得到相應的MessageVersion。這些通過兩者組合得到的MessageVersion通過靜態只讀屬性定義在MessageVersion類中。Soap11WSAddressing10、Soap11WSAddressingAugust2004、Soap12WSAddressing10和Soap12WSAddressingAugust2004的含義都是一目了然的,而None、Soap11和Soap12表示的EnvelopeVersion和Addressing組合分別是:
None:EnvelopeVersion.None + AddressingVersion.None;
Soap11:EnvelopeVersion.Soap11+ AddressingVersion.None;
Soap12:EnvelopeVersion.Soap12 + AddressingVersion.None
1: public sealed class MessageVersion
2: {
3: //其他成員
4: public static MessageVersion Default { get; }
5:
6: public static MessageVersion None { get; }
7: public static MessageVersion Soap11 { get; }
8: public static MessageVersion Soap11WSAddressing10 { get; }
9: public static MessageVersion Soap11WSAddressingAugust2004 { get; }
10: public static MessageVersion Soap12 { get; }
11: public static MessageVersion Soap12WSAddressing10 { get; }
12: public static MessageVersion Soap12WSAddressingAugust2004 { get; }
13: }
WS-Addressing是建立在SOAP之上的,所以EnvelopeVersion.None和AddressingVersion.WSAddressingAugust2004與AddressingVersion.WSAddressing10的組合是無效的。此外在MessageVersion中還定義了一個靜態只讀屬性Default,表示默認的MessageVersion,目前該值為MessageVersion.Soap12WSAddressing10。
二、如何創建消息
由於Message是一個抽象類型,不能直接實例化。Message類中定義了一系列靜態CreateMessage方法,使我們能夠方便快捷地以不同的方式進行消息的創建。對於如此眾多的CreateMessage方法,按照具體的消息創建方式的不同,大體上可以分為5類:
創建空消息;
將對象序列化成消息的主體(Body);
通過XMLWriter將內容“寫”到消息中;
通過XMLReader將內容“讀”到消息中;
創建Fault消息。
1、創建空消息
下面是所有CreateMessage靜態方法中最簡單的一個,包含兩個輸入參數:消息的版本和Action。通過該方法可以創建一個只包含Action報頭的SOAP消息。
1: public abstract class Message : IDisposable
2: {
3: //其他成員
4: public static Message CreateMessage(MessageVersion version, string action);
5: }
為演示消息的創建以及創建後的消息的結構,我寫了下面一個輔助方法WriteMessage。該方法將一個Message對象寫入一個文件中,並通過開啟進程的方式將文件打開。
1: static void WriteMessage(Message message, string fileName)
2: {
3: using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
4: {
5: message.WriteMessage(writer);
6: }
7: Process.Start(fileName);
8: }
通過下面的代碼,調用Message的CreateMessage方法,並設置消息版本為MessageVersion.Soap12WSAddressing10,Action設置為http://www.artech.com/myaction。最終將會生成如後面XML片斷所示的SOAP消息。
1: string fileName = @"E:\message.xml";
2: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction");
3: WriteMessage(message, fileName);
1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
2: <s:Header>
3: <a:Action s:mustUnderstand="1">http://www.artech.com/myaction</a:Action>
4: </s:Header>
5: <s:Body />
6: </s:Envelope>
由於消息報頭(Header)僅僅限於SOAP消息,所以如果將消息的版本改成MessageVersion.None,制定的Action不會被包含在消息中。實際上創建的Message對象不包含任何內容,最終生成的XML文件也不會包含任何文本信息。
1: string fileName = @"E:\message.xml";
2: Message message = Message.CreateMessage(MessageVersion.None, "http://www.artech.com/myaction");
3: WriteMessage(message, fileName);
2、將對象序列化成消息的主體
現在我們來關注Message的第2個重載的CreateMessage靜態方法。如下面代碼所示,該方法在上面一個重載方法的基礎上加了一個object類型的body參數,表示消息的主體(Body)。在執行該方法的時候,相應的序列化器會被調用,將對象序列化成XML並將其置於消息的主體部分。默認的序列化器就是我們在前面介紹的DataContractSerializer。
1: public abstract class Message : IDisposable
2: {
3: //其他成員
4: public static Message CreateMessage(MessageVersion version, string action, object body);
5: }
為了演示對象的序列化,我定義了下面一個數據契約Order,並定義了4個數據成員:OrderNo、OrderDate、Customer和ShipAddress。
1: [DataContract(Namespace = "http://www.artech.com")]
2: public class Order
3: {
4: [DataMember(Name = "OrderNo", Order = 1)]
5: public Guid ID
6: { get; set; }
7:
8: [DataMember(Name = "OrderDate", Order = 2)]
9: public DateTime Date
10: { get; set; }
11:
12: [DataMember(Order = 3)]
13: public string Customer
14: { get; set; }
15:
16: [DataMember(Order = 4)]
17: public string ShipAddress
18: { get; set; }
19: }
通過下面的代碼,創建Order對象,並將其傳入CreateMessage方法,作為body參數。最終將會生成如後面所示的SOAP消息。
1: string fileName = @"E:\message.xml";
2: Order order = new Order
3: {
4: ID = Guid.NewGuid(),
5: Date = DateTime.Today,
6: Customer = "Foo",
7: ShipAddress = "#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province"
8: };
9: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction", order);
10: WriteMessage(message, fileName);
1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
2: <s:Header>
3: <a:Action s:mustUnderstand="1">http://www.artech.com/myaction</a:Action>
4: </s:Header>
5: <s:Body>
6: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
7: <OrderNo>104a0213-1a0b-4d0b-b084-e912a991f908</OrderNo>
8: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
9: <Customer>Foo</Customer>
10: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
11: </Order>
12: </s:Body>
13: </s:Envelope>
從上面生成的XML,我們可以看出SOAP的主體部分就是Order對象通過DataContractSerializer序列化生成的XML。如果我們的消息不是一個SOAP消息呢?為了演示非SOAP消息的創建,我們將消息的版本替換成MessageVersion.None。從最終產生的XML結構來看,消息的整個部分就是Order對象序列化後的XML。
1: //其他代碼
2: Message message = Message.CreateMessage(MessageVersion.None, "http://www.artech.com/myaction", order);
3: WriteMessage(message, fileName);
1: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
2: <OrderNo>ae6047b2-6154-4b77-9153-6ffae03ac7c6</OrderNo>
3: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
4: <Customer>Foo</Customer>
5: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
6: </Order>
3、通過BodyWriter將內容寫入消息
接下來,我們來介紹另一個包含BodyWriter參數的CreateMessage方法重載。
1: public abstract class Message : IDisposable
2: {
3: //其他成員
4: public static Message CreateMessage(MessageVersion version, string action, BodyWriter body);
5: }
BodyWriter,顧名思義,就是消息主體的寫入器。BodyWriter是一個抽象類,定義在System.ServiceModel.Channels命名空間下,下面的代碼簡單地描述了BodyWriter的定義。構造函數參數(isBuffered)和只讀屬性IsBuffered表示消息是否被緩存。消息主體內容的寫入實現在OnWriteBodyContents方法中。
1: public abstract class BodyWriter
2: {
3: //其他成員
4: protected BodyWriter(bool isBuffered);
5: protected abstract void OnWriteBodyContents(XmlDictionaryWriter writer);
6:
7: public bool IsBuffered { get; }
8: }
為了演示基於BodyWriter的Message的創建過程,我自定義了一個簡單的BodyWriter:XmlReaderBodyWriter。實現的功能很簡單,就是從一個XML文件中讀取內容作為消息主體的內容。XmlReaderBodyWriter的定義如下:
1: public class XmlReaderBodyWriter : BodyWriter
2: {
3: private String _fileName;
4: internal XmlReaderBodyWriter(String fileName)
5: : base(true)
6: {
7: this._fileName = fileName;
8: }
9: protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
10: {
11: using (XmlReader reader = new XmlTextReader(this._fileName))
12: {
13: while (!reader.EOF)
14: {
15: writer.WriteNode(reader, false);
16: }
17: }
18: }
19: }
假設現在有一個XML文件,具有下面列出的內容(即上面演示過程中Order對象序列化的結果),文件名為E:\order.xml。
1: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
2: <OrderNo>ae6047b2-6154-4b77-9153-6ffae03ac7c6</OrderNo>
3: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
4: <Customer>FOO</Customer>
5: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
6: </Order>
那麼我們就可以通過我們定義的XmlReaderBodyWriter進行消息的創建,具體代碼實現如下所示。最終生成後面所示的SOAP消息。
1: string fileName1 = @"E:\order.xml";
2: string fileName2 = @"E:\message.xml";
3: XmlReaderBodyWriter writer = new XmlReaderBodyWriter(fileName1);
4: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction", writer);
5: WriteMessage(message, fileName2);
1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
2: <s:Header>
3: <a:Action s:mustUnderstand="1">http://www.artech.com/myaction</a:Action>
4: </s:Header>
5: <s:Body>
6: <Order xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
7: <OrderNo>104a0213-1a0b-4d0b-b084-e912a991f908</OrderNo>
8: <OrderDate>2008-12-17T00:00:00+08:00</OrderDate>
9: <Customer>FOO</Customer>
10: <ShipAddress>#328, Airport Rd, Industrial Park, Suzhou Jiangsu Province</ShipAddress>
11: </Order>
12: </s:Body>
13: </s:Envelope>
4、通過XMLReader將內容讀到消息中
如果說基於BodyWriter進行消息的創建是采用一種“推”的模式將內容寫入消息,那麼基於XMLReader的方式就是采用一種“拉”的模式。Message中定義了4個基於XmlReader的CreateMessage重載,其中兩個是直接利用XmlReader的,其余兩個則是通過XmlReader的子類XmlDictionaryReader進行消息內容的寫入。關於XmlDictionaryReader,在《WCF技術剖析(卷1)》中有詳細的介紹,對此不十分了解的讀者只需要將其理解為一個特殊的XmlReader就可以了。
1: public abstract class Message : IDisposable
2: {
3: //其他成員
4: public static Message CreateMessage(MessageVersion version, string action, XmlDictionaryReader body);
5: public static Message CreateMessage(MessageVersion version, string action, XmlReader body);
6: public static Message CreateMessage(XmlDictionaryReader envelopeReader, int maxSizeOfHeaders, MessageVersion version);
7: public static Message CreateMessage(XmlReader envelopeReader, int maxSizeOfHeaders, MessageVersion version);
8: }
在下面的程序演示中,創建一個XmlReader對象,用於讀取一個XML文件。將該XmlReader對象傳入CreateMessage方法中,該方法將會利用該XmlReader讀取相應的XML,並將其作為消息的主體部分。
1: string fileName1 = @"E:\order.xml";
2: string fileName2 = @"E:\message.xml";
3:
4: using (XmlReader reader = new XmlTextReader(fileName1))
5: {
6: Message message = Message.CreateMessage(MessageVersion.Soap12WSAddressing10, "http://www.artech.com/myaction", reader);
7: WriteMessage(message, fileName2);
8: }
5、創建Fault消息
接下來我們著重介紹如何創建一個Fault消息。在Message類中,定義了以下3個CreateMessage方法重載用以創建Fault消息。
1: public abstract class Message : IDisposable
2: {
3: //其他成員
4: public static Message CreateMessage(MessageVersion version, MessageFault fault, string action);
5: public static Message CreateMessage(MessageVersion version, FaultCode faultCode, string reason, string action);
6: public static Message CreateMessage(MessageVersion version, FaultCode faultCode, string reason, object detail, string action);
7: }
對於一個Fault消息來說,SOAP Code和SOAP Reason是必須的元素。SOAP Reason描述出錯的基本原因,通過字符串的形式表示。SOAP Code具體通過一個特殊的類System.ServiceModel.FaultCode表示,定義如下。
1: public class FaultCode
2: {
3: public FaultCode(string name);
4: public FaultCode(string name, FaultCode subCode);
5: public FaultCode(string name, string ns);
6: public FaultCode(string name, string ns, FaultCode subCode);
7:
8: public static FaultCode CreateReceiverFaultCode(FaultCode subCode);
9: public static FaultCode CreateReceiverFaultCode(string name, string ns);
10: public static FaultCode CreateSenderFaultCode(FaultCode subCode);
11: public static FaultCode CreateSenderFaultCode(string name, string ns);
12:
13: public bool IsPredefinedFault { get; }
14: public bool IsReceiverFault { get; }
15: public bool IsSenderFault { get; }
16: public string Name { get; }
17: public string Namespace { get; }
18: public FaultCode SubCode { get; }
19: }
一個完整的Fault Code由一個必需的Value元素和一個可選的SubCode元素構成(如下面的XML片段所示)。而Subcode的規范和Fualt Code一樣,也就是說Subcode是一個FaultCode,這實際上這是一個嵌套的結構。對應到FaultCode類中,屬性Name和Namepace對應Value結點的內容,而SubCode則自然對應著Fault Code的Subcode結點。如果你使用基於SOAP 1.1和SOAP 1.2的命名空間(SOAP 1.1為http://schemas. xmlsoap.org/soap/envelope/ ;SOAP 1.2為http://www.w3.org/2003/05/soap-envelope)或者是http://schemas.microsoft.com/ws/2005/05/envelope/none(相當於EnvelopeVersion.None),那麼將被視為預定義錯誤(Fault)。對於一個FaultCode,可以通過IsPredefinedFault屬性判斷是否為預定義錯誤。SOAP 1.1和SOAP 1.2定義了一些預定義的Fault Code,比如VersionMismatch、MustUnderstand、DataEncodingUnknown、Sender、Reveiver等等,其中Sender和Reveiver表示發送端和接收端導致的錯誤。FaultCode甚至定義了4個靜態的方法(CreateSenderFaultCode和CreateReceiverFaultCode)方便開發者創建這兩種特殊的FaultCode。
1: <env:Code>
2: <env:Value>env:Sender</env:Value>
3: <env:Subcode>
4: <env:Value>m:MessageTimeout</env:Value>
5: </env:Subcode>
6: </env:Code>
下面的代碼是一個典型的創建Fault消息的例子,後面給出的XML是最終生成的SOAP消息。
1: string fileName = @"E:\message.xml";
2: FaultCode subCode = new FaultCode("E0001", "http://www.artech.com/faults/");
3: FaultCode faultCode = FaultCode.CreateSenderFaultCode(subCode);
4: Message message = Message.CreateMessage(MessageVersion.Default, faultCode, "Access is denied.", "http://www.artech.com/myaction");
5: WriteMessage(message, fileName);
1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
2: <s:Header>
3: <a:Action s:mustUnderstand="1">http://www.artech.com/myaction</a:Action>
4: </s:Header>
5: <s:Body>
6: <s:Fault>
7: <s:Code>
8: <s:Value>s:Sender</s:Value>
9: <s:Subcode>
10: <s:Value xmlns:a="http://www.artech.com/faults/">a:E0001</s:Value>
11: </s:Subcode>
12: </s:Code>
13: <s:Reason>
14: <s:Text xml:lang="en-US">Access is denied.</s:Text>
15: </s:Reason>
16: </s:Fault>
17: </s:Body>
18: </s:Envelope>
除了直接通過指定Fault Code和Fault Reason創建Fault消息之外,還可以利用System.ServiceModel.Channels.MessageFault對象的方式創建。實際上,MessageFault就是Fault消息托管類型的表示。由於篇幅所限,在這裡就不做詳細介紹了,有興趣的讀者可以參閱MSDN在線文檔。