WCF技術剖析之十八:訊息契約(Message Contract)和基於訊息契約的序列化
[愛心連結:拯救一個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.MessageContractAttribute、System.ServiceModel.MessageHeaderAttribute、System.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
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
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
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
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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- WCF技術剖析之十五:資料契約代理(DataContractSurrogate)在序列化中的作用
- WCF學習(三):資料契約序列化
- WCF學習(四):資料契約的事件事件
- Programming WCF Services:資料契約版本控制
- [譯] 語句,訊息和歸約器
- Android訊息機制Message訊息池Android
- 基於WCF和MSMQ構建釋出/訂閱訊息匯流排(Pub/Sub Message Bus)薦MQ
- 遠端呼叫服務(RPC)和基於訊息的通訊(Message Queue)對比RPC
- RocketMQ 之 IoT 訊息解析:物聯網需要什麼樣的訊息技術?MQ
- Constract 契約自定義
- Laravel 的訊息佇列剖析Laravel佇列
- PHP基於Redis訊息佇列實現的訊息推送的方法PHPRedis佇列
- 契約測試Pact實踐
- 事件、契約設計與BDD事件
- 自定義訊息和對訊息的理解
- MQTT 遺囑訊息(Will Message)的使用MQQT
- 微服務測試之介面測試和契約測試微服務
- .net C#程式碼契約Contract.Require Contract.Equal等為什麼不生效?C#UI
- 理解TON合約中的訊息傳送結構
- 簽約獎金:IT世界的賣身契
- 基於xmpp openfire smack開發之Android訊息推送技術原理分析和實踐[4]MacAndroid
- 訊息資料庫Message DB:PostgreSQL的事件儲存和訊息儲存 - Eventide Blog資料庫SQL事件IDE
- ActiveMQ 中的訊息遊標(Message cursors)MQ
- MQTT-保留訊息和遺囑訊息MQQT
- PHP轉Go系列 | ThinkPHP與Gin框架之打造基於WebSocket技術的訊息推送中心PHPGo框架Web
- unity 3種message訊息管理使用Unity
- weblogic jms訊息狀態messageWeb
- 深入解析MFC訊息響應和訊息路由路由
- 再談訊息佇列技術-轉佇列
- JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇)AIKotlin
- Android訊息傳遞之Handler訊息機制Android
- Android訊息推送技術原理分析和實踐Android
- 關於Java Chassis 3的契約優先(API First)開發JavaAPI
- 深度剖析訊息反射機制 (轉)反射
- 訊息佇列之如何保證訊息的可靠傳輸佇列
- RocketMQ 訊息整合:多型別業務訊息-普通訊息MQ多型型別
- 分散式應用開發的核心技術系列之——基於TCP/IP的原始訊息設計分散式TCP
- 訊息佇列(Message Queue)基本概念佇列