創建一個消息
可以選擇眾多定義的CreateMessage工廠方法中的一個來創建Message對象。 絕大部分,這些方法接受的都是SOAP消息體的內容作為參數。非常重要的一點是 Message的body在創建以後就不能再做修改。SOAP消息頭塊,話句話說,在消息 創建以後還可以增加和修改。一般地說,Message類型的工廠方法可以根據處理 消息的路線圖來劃分的,從XmlReader提取數據的方法,把數據放進Message的方 法,和產生表示SOAP Fault Message的方法。在我們研究各個不同分類的工廠方 法之前,先看一些Message序列化和反序列化的上下文環境。
簡要介紹Message序列化和反序列化
序列化和反序列化在分布式計算裡很常見,因此,當它們牽扯到消息應用系 統的時候,就有必要弄清楚他們的意義。讓我們考慮一下基本的序列化和反序列 化Message對象的步驟。放發送程序需要發送一個Message到另外一個消息參與者 的時候,它必須創建一個包含適當信息的Message 對象,然後序列化和編碼 Message到一個Stream或Byte裡,最後發送Stream或Byte到目的地。當接收程序 收到Message的時候,一般來說,它還是Stream或Byte狀態(和發送者最後處理 的數據狀態一樣)。接收程序必須解碼和反序列化Stream或Byte[]為 Message對 象,或許還要反序列化消息頭部或者消息體為其它對象。
正如你看到的,序列化通常是與發送者規定的任務相關的,反序列化通常和 接收者的任務相關。發送者和接收者都要創建Message對象,但是發送者是內存 裡的其它對象創建Message對象,而接受者是通過解碼和反序列Stream 或 Byte 來創建一個Message對象。一旦接收程序解碼和反序列Stream 或 Byte為一個 Message對象,它就可以轉換消息的內容為一個對象,或者經過多次的對象轉換 。這個轉換,一般來說,也是另外一種形式的反序列化。
Message版本
因為Message對象是CLR對SOAP消息的抽象,而使用的SOAP消息有多種版本, 所以就需要考慮Message對象的表示的SOAP消息版本問題。
在Message對象模型裡,SOAP消息的版本一旦在創建時設定,就無法改變。 SOAP和WS-*規范在不斷的變化,因此,隨著時間推移,我們應該希望這些文檔版 本更新。隨著它們的改變,namespace 和消息結構變化也在情理之中。為了適應 這些必然發生的變化,WCF提供了幾個封裝SOAP規范和WS-*規范XML消息語義的類 型。這些類型的實例會傳遞給工廠方法來表示Message對象的版本,Message上定 義大部分工廠方法都接受這些類型的參數。
當設置Message對象時,System.ServiceModel.Channels.EnvelopeVersion類 型代表了一個Message類型遵循的SOAP規范。同時,System.ServiceModel. Channels.AddressingVersion表示Message序列化時,消息頭塊遵循的WS- Addressing規范。在WCF一次發布的時候,有2個SOAP老徐備注2規范(1.1 和1.2 ),WS-Addressing規范(2004年8月,1.0)。
System.ServiceModel.Channels.MessageVersion包裝了EnvelopeVersion 和 AddressingVersion類型。MessageVersion有幾個static屬性,它們表示 EnvelopeVersion和 AddressingVersion的幾種可能的組合。下面代碼顯示了 MessageVersion的所有公開可見的成員:
namespace System.ServiceModel.Channels {
public sealed class MessageVersion {
public AddressingVersion Addressing { get; }
public static MessageVersion Default { get; }
public EnvelopeVersion Envelope { get; }
public static MessageVersion Soap11 { get; }
public static MessageVersion Soap12 { get; }
public static MessageVersion None { get; }
public static MessageVersion Soap11WSAddressing10 { get; }
public static MessageVersion Soap11WSAddressingAugust2004 { get; }
public static MessageVersion Soap12WSAddressing10 { get; }
public static MessageVersion Soap12WSAddressingAugust2004 { get; }
public static MessageVersion CreateVersion(
EnvelopeVersion envelopeVersion);
public static MessageVersion CreateVersion(
EnvelopeVersion envelopeVersion,
AddressingVersion addressingVersion);
public override bool Equals(object obj);
public override string ToString();
}
}
大部分成員都是可以自描述的;一些需要解釋。在WCF版本3裡, MessageVersion.Default返回了等價的MessageVersion.Soap12WSAddressing10 的static屬性。正如你從名字裡看的,這個屬性表示它遵守SOAP 1.2 和WS- Addressing 1.0規范。MessageVersion.Soap11 和MessageVersion.Soap12屬性 給AddressingVersion賦值為AddressingVersion.None,並且根據屬性名字給 EnvelopeVersion賦值。當你想創建一個發送SOAP消息,但是不想實現WS- Addressing規范的程序時,這個非常有用。和你想的一樣, MessageVersion.None對於(樸樹的舊的XML)POX消息場景非常有用。
重點
當任何一個規范的發展(不可避免的),在隨後的WCF版本裡,使用 MessageVersion.Default構建Message 的代碼也會悄悄地變化。這樣也好也壞, 取決於消息場景,例如,修改WCF的版本或許也要修改MessageVersion.Default 屬性產生的 XML相關構件。如果修改真的發生,所有使用新的消息交互的消息參 與者都必須知道新的消息語義。換句話說,一個使用 MessageVersion.Default 屬性的程序或許通過升級WCF帶來巨大的變化,因此,MessageVersion.Default 屬性應該謹慎使用。
下面代碼展示了MessageVersion 老徐備注1類型引用的SOAP和WS-Addressing 版本:
using System;
using System.ServiceModel.Channels;
using System.Xml;
class Program {
static void Main(string[] args){
MessageVersion version = MessageVersion.Default;
PrintMessageVersion("Default",version);
version = MessageVersion.Soap11WSAddressing10;
PrintMessageVersion ("Soap11WSAddressing10",version);
version = MessageVersion.Soap11WSAddressingAugust2004;
PrintMessageVersion ("Soap11WSAddressingAugust2004",version);
version = MessageVersion.Soap12WSAddressing10;
PrintMessageVersion ("Soap12WSAddressing10",version);
version = MessageVersion.Soap12WSAddressingAugust2004;
PrintMessageVersion("Soap12WSAddressingAugust2004", version);
}
private static void PrintMessageVersion(String name,
MessageVersion version) {
Console.WriteLine("Name={0}\nEnvelope={1}\nAddressing={2} \n",
name,
version.Envelope.ToString(),
version.Addressing.ToString());
}
}
運行代碼產生以下輸出:
Name=Default
Envelope=Soap12 (http://www.w3.org/2003/05/soap-envelope)
Addressing=Addressing10 (http://www.w3.org/2005/08/addressing)
Name=Soap11WSAddressing10
Envelope=Soap11 (http://schemas.xmlsoap.org/soap/envelope/)
Addressing=Addressing10 (http://www.w3.org/2005/08/addressing)
Name=Soap11WSAddressingAugust2004
Envelope=Soap11 (http://schemas.xmlsoap.org/soap/envelope/)
Addressing=Addressing200408 (http://schemas.xmlsoap.org/ws/2004/08/addressing)
Name=Soap12WSAddressing10
Envelope=Soap12 (http://www.w3.org/2003/05/soap-envelope)
Addressing=Addressing10 (http://www.w3.org/2005/08/addressing)
Name=Soap12WSAddressingAugust2004
Envelope=Soap12 (http://www.w3.org/2003/05/soap-envelope)
Addressing=Addressing200408
(http://schemas.xmlsoap.org/ws/2004/08/addressing)
序列化對象
幾個CreateMessage方法是設計來序列化一個對象到Message消息體裡的。為 了這個目的,這些方法要接受System.Object類型的參數。其中一個方法使用WCF 默認的序列化器,並且另外一個接受自定義序列化器。(我們會在第9章:契約 ,裡詳細討論)此外,除了這些參數,這些方法還接受一個String類型的參數。 這個參數,在相關的Message對象的頭塊裡設置WS-Addressing Action的值。如 下所示,這些工廠方法相當簡單:
// pass a String into the factory method
Message msg = Message.CreateMessage (MessageVersion.Soap12WSAddressing10,
"urn:SomeAction",
"Hello There");
// the ToString() method returns the entire Message
Console.WriteLine(msg.ToString());
When this code executes, the following output is generated:
運行代碼產生以下輸出:
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap- envelope">
<s:Header>
<a:Action s:mustUnderstand="1">urn:SomeAction</a:Action>
</s:Header>
<s:Body>
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">
Hello There
</string>
</s:Body>
</s:Envelope>
如你在前面的例子看到的,String “Hello There”是自動賦值到Message的 消息體上。
讓我們修改Object從String參數到PurchaseOrder,如下所示,看看會發生什 麼:
sealed class MyApp {
static void Main() {
VendorInfo vInfo = new VendorInfo(5, "Contoso");
PurchaseOrder po = new PurchaseOrder(50, vInfo);
Message msg = Message.CreateMessage(
MessageVersion.Soap12WSAddressing10,
"urn:SomeAction",
po);
// the ToString() method returns the entire Message 。ToString()方法返回整個Message
Console.WriteLine(msg.ToString());
}
private sealed class PurchaseOrder {
Int32 poNumber;
VendorInfo vendorInfo;
internal PurchaseOrder(Int32 poNumber, VendorInfo vendorInfo) {
this.poNumber = poNumber;
this.vendorInfo = vendorInfo;
}
}
private sealed class VendorInfo {
Int32 vendorNumber;
String vendorName;
internal VendorInfo(Int32 vendorNumber, String vendorName) {
this.vendorNumber = vendorNumber;
this.vendorName = vendorName;
}
}
}
當代碼運行的時候,CreateMessage使用的默認的序列化器會拋出一個 InvalidDataContractException異常。我們會在第9 章裡詳細介紹數據契約和序 列化問題。如果我們想給這些方法傳遞一個對象參數,它必須可以序列化,並且 所有引用的對象也必須是可序列化的。第一個例子是 String類型的參數傳遞給 了CreateMessage方法成功執行,因為C#的基元類型都是可以序列化的。還有很 多隱式序列化的類型,也有幾個顯式指定類型序列化的方式。你也會在第9章裡 詳細了解到這些內容。現在,我們來給PurchaseOrder和VendorInfo類型標記 SerializableAttribute屬性,這樣可以使得他們支持序列化。如果我們使用可 序列化類型運行前面的代碼,我們會看到下面的輸出:
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
xmlns:s="http://www.w3.org/2003/05/soap- envelope">
<s:Header>
<a:Action s:mustUnderstand="1">urn:SomeAction</a:Action>
</s:Header>
<s:Body>
<MyApp.PurchaseOrder
xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns=
"http://schemas.datacontract.org/2004/07/CreatingMessageBySerializatio n">
<poNumber>50</poNumber>
<vendorInfo>
<vendorName>Contoso</vendorName>
<vendorNumber>5</vendorNumber>
</vendorInfo>
</MyApp.PurchaseOrder>
</s:Body>
</s:Envelope>
注意到一個PurchaseOrder對象(一個VendorInfo對象)序列化到SOAP消息體 裡。
從Reader提取數據
幾個CreateMessage方法接受一個XmlReader或XmlDictionaryReader作為參數 。這些方法“pull” XmlDictionaryReader的整個內容到返回的Message對象裡 ,或者到Message的body裡(消息體)。著重指出的是CreateMessage方法接受一 個XmlReader作為參數,通過調用XmlDictionaryReader 的 CreateDictionaryReade工廠方法來創建一個XmlDictionaryReader對象。
這些方法非常有用,當你要把Message反序列化到一個Byte[] 或者Stream裡 的時,這是特別常見的,尤其是當接收程序接受一個包含序列化和編碼過的數據 的Stream時。當你使用這些方法構建一個Message對象,你必須知道Byte[]或 Stream是包含了全部消息還是僅僅是消息體。為了適應2種情況,CreateMessag 方法被重載為多個方法,他們包含了能讀取整個消息和只讀取消息體的參數。
為了說明問題,下面的文件包含了已經被序列化到entireMessage.xml文件裡 的消息內容:
<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/
05/soap-envelope">
<s:Header>
<a:Action s:mustUnderstand="1">urn:SomeAction</a:Action>
</s:Header>
<s:Body>
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">
Hello Message
</string>
</s:Body>
</s:Envelope>
同樣,下面是一個包含消息體的文件名bodyContent.xml:
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">
Hello Message
</string>
下面代碼例子展示了如何使這些資源構建消息:
const Int32 MAXHEADERSIZE = 500;
// ENVELOPE READER EXAMPLE
// Get data from the file that contains the entire message
FileStream stream = File.Open("entireMessage.xml", FileMode.Open);
XmlDictionaryReader envelopeReader =
XmlDictionaryReader.CreateTextReader(stream, new
XmlDictionaryReaderQuotas());
Message msg = Message.CreateMessage(envelopeReader,
MAXHEADERSIZE,
MessageVersion.Soap12WSAddressing10);
Console.WriteLine("{0}\n", msg.ToString());
//BODY READER EXAMPLE消息體讀取的例子
// Get data from a file that contains just the body從 文件裡只讀消息體數據
stream = File.Open("bodyContent.xml", FileMode.Open);
XmlDictionaryReader bodyReader =
XmlDictionaryReader.CreateTextReader(stream, new
XmlDictionaryReaderQuotas());
msg = Message.CreateMessage (MessageVersion.Soap12WSAddressing10,
"urn:SomeAction", bodyReader);
Console.WriteLine("{0}\n", msg.ToString());
注意第一次調用CreateMessage方法,接受一個表示最大Message 消息頭字節 的Int32參數。這個限制是必要的,因為我們要從一個Stream序列化Message的內 容。因為消息頭經常是緩存的,這個參數允許我們控制緩存區大小和處理 Message需要的資源。因為第二次調用CreateMessage方法只從Stream裡讀取消息 體的內容,控制消息頭緩存區大小就沒有必要了。
使用BodyWriter把數據放進Message。
其中一個CreateMessage的重載方法允許調用者使用 System.ServiceModel.Channels.BodyWriter 把數據“push”到Message裡。 BodyWriter是一個抽象類型,它暴露了一個接受XmlDictionaryWriter 作為參數 的保護方法。BodyWriter的一個子類型完全控制Message 消息體的創建過程,因 此BodyWriter對於控制Message的反序列化非常有用。絕大部分上, OnWriteBodyContents方法的實現包含調用XmlDictionaryWriter參數上的不同的 Write方法。下面的例子演示了BodyWriter子類型如何從XML文件裡讀取數據並放 進Message的消息體裡:
sealed class MyBodyWriter : BodyWriter {
private String m_fileName;
internal MyBodyWriter(String fileName) : base(true) {
this.m_fileName = fileName;
}
protected override void OnWriteBodyContents(XmlDictionaryWriter writer) {
using (FileStream stream = File.Open(m_fileName, FileMode.Open)) {
XmlDictionaryReader reader1 =
XmlDictionaryReader.CreateTextReader(stream, new
XmlDictionaryReaderQuotas());
reader1.ReadStartElement();
while (reader1.NodeType != XmlNodeType.EndElement) {
writer.WriteNode(reader1, true);
}
}
}
}
Once the BodyWriter is subclassed, it can be used in a CreateMessage method, as shown here:
Message pushMessage = Message.CreateMessage(
MessageVersion.Soap12WSAddressing10,
"urn:SomeAction",
new MyBodyWriter ("bodyContent.xml"));
(下半部分會介紹 SOAP Fault 和MessageFault )