在本篇文章中,我們將討論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/ProcessOrder</a:Action>
4: <h:CustomerName xmlns:h="http://www.artech.com/">Foo</h:CustomerName>
5: <h:CustomerNo xmlns:h="http://www.artech.com/">2f62405b-a472-4d1c-8c03-b888f9bd0df9</h: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 Province</Address>
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 Province</Address>
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 Province</Address>
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-4ba9bfef3d4d</h: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-cd16be4c75b7</h: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<OrderDetail>
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, TContract>。TFormatter代表MessageFormatter的兩個接口:IClientMessageFormatter和IDispatchMessageFormatter(DataContractSerializerOperationFormatter同時實現了這兩個接口),TContract則是服務契約的類型。參數operationName為當前操作的名稱。代碼不算復雜,主要的流程如下:通過服務契約類型創建ContractDescription,根據操作名稱得到OperationDescription對象。通過反射機制調用DataContractSerializerOperationFormatter的構造函數創建該對象。
1: static TFormatter CreateMessageFormatter<TFormatter, TContract>(string operationName)
2: {
3: ContractDescription contractDesc = ContractDescription.GetContract(typeof(TContract));
4: var operationDescs = contractDesc.Operations.Where(op => op.Name == operationName);
5: if(operationDescs.Count() == 0)
6: {
7: throw new ArgumentException("operationName","Invalid operation name.");
8: }
9: OperationDescription operationDesc = 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<TContract>和DeserializeRequest<TContract>,具體實現就是調用創建出來的MessageFormatter的同名方法。
1: static Message SerializeRequest<TContract>(MessageVersion messageVersion, string operationName, params object[] values)
2: {
3: IClientMessageFormatter formatter = CreateMessageFormatter<IClientMessageFormatter, TContract>(operationName);
4: return formatter.SerializeRequest(messageVersion, values);
5: }
6:
7: static void DeserializeRequest<TContract>(Message message, string operationName, object[] parameters)
8: {
9: IDispatchMessageFormatter formatter = CreateMessageFormatter<IDispatchMessageFormatter, TContract>(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<IOrderManager>方法將Order對象進行序列化並生成Message對象,該過程實際上體現了WCF的客戶端框架是如何通過ClientMessageFormatter將操作方法調用連同輸入參數轉換成請求消息的。隨後,調用DeserializeRequest<IOrderManager>方法將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 order = new Order
14: {
15: OrderID = Guid.NewGuid(),
16: Date = DateTime.Today,
17: Details = new OrderDetails { detail1, detail2 }
18: };
19: //模擬WCF客戶端的序列化
20: Message message = SerializeRequest<IOrderManager>(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<IOrderManager>(buffer.CreateMessage(), "ProcessOrder", DeserializedOrder);
27: Console.WriteLine(DeserializedOrder[0]);
下面的XML表示調用SerializeRequest<IOrderManager>生成的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/ProcessOrder</a:Action>
4: <h:Date xmlns:h="http://www.artech.com/">2008-12-21T00:00:00+08:00</h:Date>
5: <h:OrderID xmlns:h="http://www.artech.com/">cd94a6f0-7e21-4ace-83f7-2ddf061cfbbe</h: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-3693248104c0</d4p1:ProductID>
12: <d4p1:Quantity>666</d4p1:Quantity>
13: </d4p1:Detail>
14: <d4p1:Detail>
15: <d4p1:ProductID>72687c23-c2b2-4451-b6c3-da6d040587fc</d4p1:ProductID>
16: <d4p1:Quantity>999</d4p1: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