WCF技术剖析之十八:消息契约(Message Contract)和基于消息契约的序列化

时间:2022-12-26 21:12:47

[爱心链接:拯救一个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/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
作者: Artech
出处: http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。