WCF技術剖析之十八:訊息契約(Message Contract)和基於訊息契約的序列化

iDotNetSpace發表於2009-08-03

[愛心連結:拯救一個25歲身患急性白血病的女孩[內有蘇州電視臺經濟頻道《天天山海經》為此錄製的節目視訊(蘇州話)]]在本篇文章中,我們將討論WCF四大契約(服務契約、資料契約、訊息契約和錯誤契約)之一的訊息契約(Message Contract)。服務契約關注於對服務操作的描述,資料契約關注於對於資料結構和格式的描述,而訊息契約關注的是型別成員與訊息元素的匹配關係。

我們知道只有可序列化的物件才能通過服務呼叫在客戶端和服務端之間進行傳遞。到目前為止,我們知道的可序列化型別有兩種:一種是應用了System.SerializableAttribute特性或者實現了System.Runtime.Serialization.ISerializable介面的型別;另一種是資料契約物件。對於基於這兩種型別的服務操作,客戶端通過System.ServiceModel.Dispatcher.IClientMessageFormatter將輸入引數格式化成請求訊息,輸入引數全部內容作為有效負載置於訊息的主體中;同樣地,服務操作的執行結果被System.ServiceModel.Dispatcher.IDispatchMessageFormatter序列化後作為回覆訊息的主體。

在一些情況下,具有這樣的要求:當序列化一個物件並生成訊息的時候,希望將部分資料成員作為SOAP的報頭,部分作為訊息的主體。比如說,我們有一個服務操作採用流的方式進行檔案的上載,除了以流的方式傳輸以二進位制表示的檔案內容外,還需要傳輸一個額外的基於檔案屬性的資訊,比如檔案格式、檔案大小等。一般的做法是將傳輸檔案內容的流作為SOAP的主體,將其屬性內容作為SOAP的報頭進行傳遞。這樣的功能,可以通過定義訊息契約來實現。

一、 訊息契約的定義

訊息契約和資料契約一樣,都是定義在資料(而不是功能)型別上。不過資料契約旨在定義資料的結構(將資料型別與XSD進行匹配),而訊息契約則更多地關注於資料的成員具體在SOAP訊息中的表示。訊息契約通過以下3個特性進行定義:System.ServiceModel.MessageContractAttributeSystem.ServiceModel.MessageHeaderAttributeSystem.ServiceModel.MessageBodyMemberAttribute。MessageContractAttribute應用於型別上,MessageHeaderAttribute和MessageBodyMemberAttribute則應用於屬性或者欄位成員上,表明相應的資料成員是一個基於SOAP報頭的成員還是SOAP主體的成員。先來簡單介紹一下這3個特性:

1、MessageContractAttribute

通過在一個類或者結構(Struct)上應用MessageContractAttribute使之成為一個訊息契約。從MessageContractAttribute的定義來看,MessageContractAttribute大體上具有以下兩種型別的屬性成員:

  • ProtectionLevel和HasProtectionLevel:表示保護級別,在服務契約中已經對保護級別作了簡單的介紹,WCF中通過System.Net.Security.ProtectionLevel列舉定義訊息的保護級別。一般有3種可選的保護級別:None、Sign和EncryptAndSign
  • IsWrapped、WrapperName、WrapperNamespace:IsWrapped表述的含義是是否為定義的主體成員(一個或者多個)新增一個額外的根節點。WrapperName和WrapperNamespace則表述該根節點的名稱和名稱空間。IsWrapped、WrapperName、WrapperNamespace的預設是分別為true、型別名稱和http://tempuri.org/
   1: [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = false)]
   2: public sealed class MessageContractAttribute : Attribute
   3: {  
   4:     //其他成員
   5:     public bool         HasProtectionLevel { get; }
   6:     public ProtectionLevel    ProtectionLevel { get; set; }
   7:  
   8:     public bool     IsWrapped { get; set; }
   9:     public string     WrapperName { get; set; }
  10:     public string     WrapperNamespace { get; set; }
  11: }

下面的程式碼中將Customer型別通過應用MessageContractAttribute使之成為一個訊息契約。ID和Name屬性通過應用MessageHeaderAttribute定義成訊息報頭(Header)成員,而Address屬性則通過MessageBodyMemberAttribute定義成訊息主體(Body)成員。後面的XML體現的是Customer物件在SOAP訊息中的表現形式。

   1: [MessageContract]
   2: public class Customer
   3: {
   4:     [MessageHeader(Name = "CustomerNo", Namespace = "http://www.artech.com/")]
   5:     public Guid ID
   6:     { get; set; }
   7:  
   8:     [MessageHeader(Name = "CustomerName", Namespace = "http://www.artech.com/")]
   9:     public string Name
  10:     { get; set; }
  11:  
  12:     [MessageBodyMember(Namespace = "http://www.artech.com/")]
  13:     public string Address
  14:     { get; set; }
  15: }
   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://tempuri.org/IOrderManager/ProcessOrdera:Action>
   4:         <h:CustomerName xmlns:h="http://www.artech.com/">Fooh:CustomerName>
   5:         <h:CustomerNo xmlns:h="http://www.artech.com/">2f62405b-a472-4d1c-8c03-b888f9bd0df9h:CustomerNo>
   6:     s:Header>
   7:     <s:Body>
   8:         <Customer xmlns="http://tempuri.org/">
   9:             <Address xmlns="http://www.artech.com/">#328, Airport Rd, Industrial Park, Suzhou Jiangsu ProvinceAddress>
  10:         Customer>
  11:     s:Body>
  12: s:Envelope> 

如果我們將IsWrapped的屬性設為false,那麼套在Address節點外的Customer節點將會從SOAP訊息中去除。

   1: [MessageContract(IsWrapped = false)]
   2: public class Customer
   3: {
   4:       //省略成員
   5: }
   1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
   2:     ......
   3:     <s:Body>
   4:         <Address xmlns="http://www.artech.com/">#328, Airport Rd, Industrial Park, Suzhou Jiangsu ProvinceAddress>
   5:     s:Body>
   6: s:Envelope>

我們同樣可以自定義這個主體封套(Wrapper)的命名和名稱空間。下面我們就通過將MessageContractAttribute的WrapperName和WrapperNamespace屬性設為Cust和http://www.artech.com/。

   1: [MessageContract(IsWrapped = true, WrapperName = "Cust", WrapperNamespace = "http://www.artech.com/")]
   2: public class Customer
   3: {
   4:     //省略成員
   5: }
   1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
   2:     ......
   3:     <s:Body>
   4:         <Cust xmlns="http://www.artech.com/">
   5:             <Address>#328, Airport Rd, Industrial Park, Suzhou Jiangsu ProvinceAddress>
   6:         Cust>
   7:     s:Body>
   8: s:Envelope>

2、MessageHeaderAttribute

MessageHeaderAttribute和MessageBodyMemberAttribute分別用於定義訊息報頭成員和訊息主體成員,它們都有一個共同的基類:System.ServiceModel.MessageContractMemberAttribute。MessageContractMemberAttribute定義了以下屬性成員:HasProtectionLevel、ProtectionLevel、Name和Namespace。

   1: public abstract class MessageContractMemberAttribute : Attribute
   2: {   
   3:     public bool             HasProtectionLevel { get; }
   4:     public ProtectionLevel     ProtectionLevel { get; set; }
   5:  
   6:     public string             Name { get; set; }
   7:     public string             Namespace { get; set; }
   8: }

通過在屬性或者欄位成員上應用MessageHeaderAttribute使之成為一個訊息報頭成員。MessageHeaderAttribute定義了以下3個屬性,如果讀者對SOAP規範有一定了解的讀者,相信對它們不會陌生。

注:在《WCF技術剖析(卷1)》中的第六章有對SOAP 1.2的基本規範有一個大致的介紹,讀者也可以直接訪問W3C網站下載官方文件。

  • Actor:表示處理該報頭的目標節點(SOAP Node),SOAP1.1中對應的屬性(Attribute)為actor,SOAP 1.2中就是我們介紹的role屬性
  • MustUnderstand:表述Actor(SOAP 1.1)或者Role(SOAP 1.2)定義的SOAP節點是否必須理解並處理該節點。對應的SOAP報頭屬性為mustUnderstand
  • Relay:對應的SOAP報頭屬性為relay,表明該報頭是否需要傳遞到下一個SOAP節點
   1: [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
   2: public class MessageHeaderAttribute : MessageContractMemberAttribute
   3: {
   4:     public string     Actor { get; set; }
   5:     public bool     MustUnderstand { get; set; }
   6:     public bool     Relay { get; set; }
   7: }

同樣使用上面定義的Customer訊息契約,現在我們相應地修改了ID屬性上的MessageHeaderAtribute設定:MustUnderstand = true, Relay=true, Actor=http://www.w3.org/ 2003/05/soap-envelope/role/ultimateReceiver。實際上將相應的SOAP報頭的目標SOAP節點定義成最終的訊息接收者。由於http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver是SOAP 1.2的預定義屬性,所以這個訊息契約之後在基於SOAP 1.2的訊息版本中有效。後面給出的為對應的SOAP訊息。

   1: [MessageContract(IsWrapped =true, WrapperNamespace="http://www.artech.com/")]public class Customer
   2: {
   3:     //其他成員
   4:     [MessageHeader(Name="CustomerNo", Namespace = "http://www.artech.com/" ,MustUnderstand = true, Relay=true, Actor="http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver" )]
   5:     public Guid ID
   6:     { get; set; }
   7:     
   8: }
   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:         ......
   4:         <h:CustomerNo s:role="http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver" s:mustUnderstand="1" s:relay="1" xmlns:h="http://www.artech.com/">5330c91a-7fd7-4bf5-ae3e-4ba9bfef3d4dh:CustomerNo>
   5:     s:Header>
   6: ......
   7: s:Envelope>

http://www.w3.org/2003/05/soap-envelope/role/ultimateReceiver在SOAP1.1中對應的表示為:"http://schemas.xmlsoap.org/soap/actor/ultimateReceiver(具有不同的名稱空間)。如果在SOAP 1.1下,ID成員對應的MessageHeaderAttribute應該做如下的改動。從對應的SOAP訊息來看,在SOAP 1.2中的role屬性變成了actor屬性。

   1: [MessageContract(IsWrapped =true, WrapperNamespace="http://www.artech.com/")]public class Customer
   2: {
   3:     //其他成員
   4:     [MessageHeader(Name="CustomerNo", Namespace = "http://www.artech.com/" ,MustUnderstand = true, Relay=true, Actor="http://schemas.xmlsoap.org/soap/actor/ultimateReceiver" )]
   5:     public Guid ID
   6:     { get; set; }    
   7: }
   1: <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   2:     <s:Header>
   3:         ......
   4:         <h:CustomerNo s:actor="http://schemas.xmlsoap.org/soap/actor/ultimateReceiver" s:mustUnderstand="1" xmlns:h="http://www.artech.com/">e48a8897-c644-49f8-b5e7-cd16be4c75b7h:CustomerNo>
   5:     s:Header>
   6:     ......
   7: s:Envelope>

3、MessageBodyMemberAttribute

MessageBodyMemberAttribute應用於屬性或者欄位成員,應用了該特性的屬性或者欄位的內容將會出現在SOAP的主體部分。MessageBodyMemberAttribute的定義顯得尤為簡單,僅僅具有一個Order物件,用於控制成員在SOAP訊息主體中出現的位置。預設的排序規則是基於字母排序。

可能細心的讀者會問,為什麼MessageHeaderAttribute中沒有這樣Order屬性呢?原因很簡單,MessageHeaderAttribute定義的是單個SOAP報頭,SOAP訊息報頭集合中的每個報頭元素是次序無關的。而MessageBodyMemberAttribute則是定義SOAP主體的某個元素,主體成員之間的次序也是契約的一個重要組成部分。所以MessageHeaderAttribute不叫MessageHeaderMemberAttribute。

   1: [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false)]
   2: public class MessageBodyMemberAttribute : MessageContractMemberAttribute
   3: {
   4:     public int Order { get; set; }
   5: }

二、例項演示:基於訊息契約的方法呼叫是如何格式化成訊息的?

在WCF體系中,MessageFormatter負責序列化和反序列化任務(在《WCF技術剖析(卷1)》中的第5章對基於MessageFormatter的序列化機制有詳細的介紹):ClientMessageFormatter和DispatchMessageFormatter分別在客戶端和服務端,根據操作的描述(Operation Description),藉助於相應的序列化器(Serializer)實現了方法呼叫與訊息之間的轉換。接下來,我將通過一個實實在在的案例程式為大家演示如何通過ClientMessageFormatter將輸入引數轉換為基於當前服務操作的Message。由於本節的主題是訊息契約,所以在這裡我們將轉換物件限定為訊息契約。不過,不論是訊息引數還是一般的可序列化物件,其轉換過程都是一樣的。

步驟一:建立訊息契約

本案例模擬一個訂單處理的WCF應用,我們首先定義如下一個Order型別。Order是一個訊息契約,屬性OrderID和Date通過MessageHeaderAttribute定義成訊息報頭,作為主體的Details的型別OrderDetails被定義成集合資料契約。OrderDetails的元素型別是資料契約OrderDetail,代表訂單中每筆產品明細。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Runtime.Serialization;
   4: using System.ServiceModel;
   5: namespace Artech.TypedMessage
   6: {
   7:     [MessageContract]
   8:     public class Order
   9:     {
  10:         [MessageHeader(Namespace ="http://www.artech.com/")]
  11:         public Guid OrderID
  12:         { get; set; }
  13:  
  14:         [MessageHeader(Namespace ="http://www.artech.com/")]
  15:         public DateTime Date
  16:         { get; set; }
  17:  
  18:         [MessageBodyMember]
  19:         public OrderDetails Details
  20:         { get; set; }
  21:  
  22:       public override string ToString()
  23:         {
  24:             return string.Format("Oder ID: {0}\nDate: {1}\nDetail Count: {2}",this.OrderID,this.Date.ToShortDateString(),this.Details.Count);
  25:         }
  26:     }
  27:  
  28:     [CollectionDataContract(ItemName = "Detail",Namespace ="http://www.artech.com/")]
  29:     public class OrderDetails : List
  30:     { }
  31:  
  32:     [DataContract(Namespace ="http://www.artech.com/")]
  33:     public class OrderDetail
  34:     {
  35:         [DataMember]
  36:         public Guid ProductID
  37:         { get; set; }
  38:  
  39:         [DataMember]
  40:         public int Quantity
  41:         { get; set; }
  42:     }
  43: }

步驟二:建立MessageFormatter

本例的目的在於重現WCF如何通過ClientMessageFormatter實現將輸入引數序列化成請求訊息,以及通過DispatchMessageFormatter實現將請求訊息反序列化成輸入引數。根據使用的序列化器的不同,WCF中定義了兩種典型的MessageFormatter:一種是基於DataContractSerializer的DataContractSerializerOperationFormatter;另一種則是基於XmlSerializer的XmlSerializerOperationFormatter。由於DataContractSerializerOperationFormatter是預設的MessageFormatter,所以我們這個案例就採用DataContractSerializerOperationFormatter。

我們的任務就是建立這個DataContractSerializerOperationFormatter。由於這是一個定義在System.ServiceModel.Dispatcher名稱空間下的內部(internal)型別,所以我們只能通過反射的機制呼叫建構函式來建立這個物件。DataContractSerializerOperationFormatter定義了唯一的一個建構函式,3個輸入引數型別分別為:OperationDescription,DataContractFormatAttribute和DataContractSerializerOperationBehavior。

   1: internal class DataContractSerializerOperationFormatter : OperationFormatter
   2: {    
   3:     //其他成員
   4:     public DataContractSerializerOperationFormatter(OperationDescription description, DataContractFormatAttribute dataContractFormatAttribute, DataContractSerializerOperationBehavior. serializerFactory);
   5: }

為此我們定義下面一個輔助方法CreateMessageFormatter。TFormatter代表MessageFormatter的兩個介面:IClientMessageFormatter和IDispatchMessageFormatter(DataContractSerializerOperationFormatter同時實現了這兩個介面),TContract則是服務契約的型別。引數operationName為當前操作的名稱。程式碼不算複雜,主要的流程如下:通過服務契約型別建立ContractDescription,根據操作名稱得到OperationDescription物件。通過反射機制呼叫DataContractSerializerOperationFormatter的建構函式建立該物件。

   1: static TFormatter CreateMessageFormatter(string operationName)
   2: {
   3:     ContractDescription contractDesc = ContractDescription.GetContract(typeof(TContract));
   4:     var perationDescs = contractDesc.Operations.Where(op => op.Name == operationName);
   5:     if(operationDescs.Count() == 0)
   6:     {
   7:        throw new ArgumentException("operationName","Invalid operation name.");
   8:     }
   9:     OperationDescription perationDesc = operationDescs.ToArray()[0];
  10:     string formatterTypeName = "System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter,System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
  11:     Type formatterType = Type.GetType(formatterTypeName);
  12:     ConstructorInfo constructor = formatterType.GetConstructor(new Type[] { typeof(OperationDescription), typeof(DataContractFormatAttribute), typeof(DataContractSerializerOperationBehavior) });
  13: return (TFormatter)constructor.Invoke(new object[] { operationDesc, new DataContractFormatAttribute(), null });
  14: }   

MessageFormatter已經建立出來了,序列化與反序列化的問題就很簡單了。為此我定義了以下兩個輔助方法:SerializeRequest和DeserializeRequest,具體實現就是呼叫建立出來的MessageFormatter的同名方法。

   1: static Message SerializeRequest(MessageVersion messageVersion, string operationName, params object[] values)
   2: {
   3:     IClientMessageFormatter formatter = CreateMessageFormatter(operationName);
   4:     return formatter.SerializeRequest(messageVersion, values);
   5: } 
   6:  
   7: static void DeserializeRequest(Message message, string operationName, object[] parameters)
   8: {
   9:     IDispatchMessageFormatter formatter = CreateMessageFormatter(operationName);
  10:     formatter.DeserializeRequest(message, parameters);
  11: }

步驟三:通過MessageFormmatter實現訊息的格式化

現在我們通過一個簡單的例子來演示通過上面建立的MessageFormatter實現對訊息的格式化。由於MessageFormatter進行序列化和反序列化依賴於操作的描述(訊息的結構本來就是由操作決定的),為此我們定義了一個服務契約IOrderManager。操作ProcessOrder將訊息契約Order作為唯一的引數。

   1: using System.ServiceModel;
   2: namespace Artech.TypedMessage
   3: {
   4:     [ServiceContract]
   5:     public interface IOrderManager
   6:     {
   7:         [OperationContract]
   8:         void ProcessOrder(Order order);
   9:     }
  10: }

在下面的程式碼中,先呼叫SerializeRequest方法將Order物件進行序列化並生成Message物件,該過程實際上體現了WCF的客戶端框架是如何通過ClientMessageFormatter將操作方法呼叫連同輸入引數轉換成請求訊息的。隨後,呼叫DeserializeRequest方法將Message物件反序列化成Order物件,該過程則代表WCF的服務端框架是如何通過DispatchMessageFormatter將請求訊息反序列化成輸入引數的。

   1: OrderDetail detail1 = new OrderDetail
   2: {
   3:     ProductID = Guid.NewGuid(),
   4:     Quantity = 666
   5: }; 
   6:  
   7: OrderDetail detail2 = new OrderDetail
   8: {
   9:     ProductID = Guid.NewGuid(),
  10:     Quantity = 999
  11: }; 
  12:  
  13: Order rder = new Order
  14: {
  15:     rderID = Guid.NewGuid(),
  16:     Date = DateTime.Today,
  17:     Details = new OrderDetails { detail1, detail2 }
  18: }; 
  19: //模擬WCF客戶端的序列化
  20: Message message = SerializeRequest(MessageVersion.Default, "ProcessOrder", order);
  21: MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
  22: WriteMessage(buffer.CreateMessage(), "message.xml"); 
  23:  
  24: //模擬WCF服務端的反序列化
  25: object[] DeserializedOrder = new object[]{ null };
  26: DeserializeRequest(buffer.CreateMessage(), "ProcessOrder", DeserializedOrder);
  27: Console.WriteLine(DeserializedOrder[0]);

下面的XML表示呼叫SerializeRequest生成的SOAP訊息。程式最終的輸出結果也表明了反序列化的成功執行。

   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://tempuri.org/IOrderManager/ProcessOrdera:Action>
   4:         <h:Date xmlns:h="http://www.artech.com/">2008-12-21T00:00:00+08:00h:Date>
   5:         <h:OrderID xmlns:h="http://www.artech.com/">cd94a6f0-7e21-4ace-83f7-2ddf061cfbbeh:OrderID>
   6:     s:Header>
   7:     <s:Body>
   8:         <Order xmlns="http://tempuri.org/">
   9:             <Details xmlns:d4p1="http://www.artech.com/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
  10:                 <d4p1:Detail>
  11:                     <d4p1:ProductID>bc2a186d-569a-4146-9b97-3693248104c0d4p1:ProductID>
  12:                     <d4p1:Quantity>666d4p1:Quantity>
  13:                 d4p1:Detail>
  14:                 <d4p1:Detail>
  15:                     <d4p1:ProductID>72687c23-c2b2-4451-b6c3-da6d040587fcd4p1:ProductID>
  16:                     <d4p1:Quantity>999d4p1:Quantity>
  17:                 d4p1:Detail>
  18:             Details>
  19:         Order>
  20:     s:Body>
  21: s:Envelope>
   1: Oder ID: cd94a6f0-7e21-4ace-83f7-2ddf061cfbbe
   2: Date: 12/21/2008
   3: Detail Count: 2
原文地址:http://www.cnblogs.com/artech/archive/2009/08/02/1536811.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-611138/,如需轉載,請註明出處,否則將追究法律責任。

相關文章