在这篇文章中,将会包括:
契约通常发生在业务事务中,以约束参与者的互操作行为。对分布式通信服务,契约在确保服务消费者能够和服务提供者正确的交互起着非常重要的作用。环顾四周,我们会发现术语SOA(Service-Oriented Architecture)被广泛使用。从技术上讲,SOAP (Simple Object Access Protocol)可以说是一套可被调用的完整部件,并且完整的接口描述能够被发布和发现。从SOA的角度,通过正确定义契约,服务消费者能够了解如何使用目标服务,而不用知道该服务是如何实现的。
作为统一的通信编程平台,WCF在WCF服务开发的各个部分为契约相关的设计提供了完整的支持。包括ServiceContract, OperationContract, DataContract, MessageContract, FaultContract等等。ServiceContract和OperationContract被用来表示一个服务和这个服务的操作定义(类似于操作集合和操作签名)。DataContract被用来表示在服务端和客户端之间一致的数据交换。如果开发者想要完全控制数据在服务端和客户端的数据传输方式,可以使用MessageContract控制基础的服务信息。WCF也为开发者提供了FaultContract,用来在特定的服务中显式关联用户自定义的异常类型,这样当一个错误发生的时候相应的错误信息将被返回。
本文共用七个部分来介绍在WCF服务开发中各类契约是如何工作的。包括定义一个one-way服务操作,帮助你熟悉标准的ServiceContract和OperationContract声明。接下来,讲解如何通过确定服务操作需要的自定义错误格式,使用FaultContractAttribute关联一个自定义SOAP错误数据类型。在第三、四、五部分,我们将集中在DataContract主题上,诸如DataContract版本控制,使用XMLSerializer序列化DataContract类型,为生成DataContract的契约优先方法。最后两个部分描述了如何在服务操作中使用MessageContract完成消息格式化的低级操作,比如返回任意XML数据作为消息内容,通过MessageContract类成员添加一个自定义SOAPHeader。
One-way操作在分布式编程中是一种普通的模式,也是WCF支持的3种消息交换模式的一种。当使用one-way消息交换模式的时候,客户端发送一条消息将使用一次fire-and-forget交换(参见下图)。Fire-and-forget交换需要额外发送一次成功确认请求。消息可能在传输过程中丢失并且永远无法到达服务端。如果发送操作在客户端成功完成,不能保证远程终结点已经接受到消息。在客户端只用发送消息给服务端,而不用照顾到执行结果的情况下,我们可以考虑定义我们的WCF服务操作为one-way风格。
下面的代码片段展示了完整的one-way OperationContract定义:
[ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true)] void OneWayMethod() { // Do some work here } }
当用IsOneWay=true标记OperationContract时,运行时将会检测到并且知道这个服务操作需要使用one-way风格。One-way操作不能带回一个返回值,而且只能够传递输入参数到服务端。在客户端发送服务请求之后,客户端只能等待,直到请求信息到达服务端,客户端才能获得响应信息。然而,这里的响应信息是没有返回值的,但是有通信协议级别的返回值,显示为请求已到达服务端(但是不知道请求是否被处理或怎样被处理了)。
我们通过下面的问题来更进一步理解one-way 操作:
一个标准的void(没有返回值)操作和一个one-way操作有什么不同?
假设已经有下面的ServiceContract实现:
[ServiceContract] public interface IHelloService { [OperationContract(IsOneWay = false)] void DoWork(); [OperationContract(IsOneWay = true)] void DoWorkAsOneWay(); }
通过客户端调用这两个操作捕获的HTTP信息(这里使用Fiddler2进行捕获),我们能够得到不同的响应信息(见以下两张截图)。第一张截图显示DoWork操作的响应信息,接下来那张截图显示了DoWorkAsOneWay操作的响应信息。
正如看到的,一般的void操作返回HTTP 200状态码,并且有完整的SOAP响应信息在body中,然而one-way操作只返回一个HTTP 202 Accepted状态头部信息。这表明one-way操作只要服务端已接收请求即完成一次服务调用,而通常一般的void操作(标准请求/响应)会等待服务端执行并返回响应数据。理解了这些能够帮助我们关于是否使用one-way操作做出更好的决定。
除了one-way操作,还有其他两种消息交换模式在WCF服务中被广泛使用。他们是Request-response(请求-响应)模式和Duplex(双工)模式。Request-response模式和传入参数返回值的标准函数调用非常相似。在基于Request-response模式的WCF服务操作调用中,发送一条消息,同时将收到一条答复。这个模式由一对请求-响应组成,见下图。
Duplex交换模式允许一个客户端发送任意数量的消息,并且以任何顺序接受。这个模式就像电话交谈,任何一句说出来的话都是一个消息(参考下图)。
WCF默认使用DataContractSerializer作为序列化引擎来序列化和反序列化数据。如果想要在一个WCF服务中添加新的复杂数据类型(用来在服务操作中传输),我们需要定义为DataContract类型,这是对DataContractSerializer引擎友好的定义方式。一个.NET序列化系统对自定义数据类型支持自然地向后兼容(backward-compatibility)。然而,有时我们也需要在WCF服务中使用向前兼容(forward-compatibility)的数据类型。假设一个服务和客户端交换一些自定义数据类型。如果一边变更了自定义数据类型(添加一些字段或属性)或者使用一个更新的版本,在确保使用了最新的数据类型实例的情况下,另一边(没有使用最新版本的数据类型)仍然能够正确工作是非常重要的。
[DataContract] public class FCQuestion : IExtensibleDataObject { [DataMember] public string Subject { get; set; } [DataMember] public string Answer { get; set; } public ExtensionDataObject ExtensionData { get; set; } }
你可以看一看关于ServiceBehaviorAttribute文章。
关于IgnoreExtensionDataObject Property的更多信息在:
http://msdn.microsoft.com/en-us/library/system.servicemodel.servicebehaviorattribute.ignoreextensiondataobject.aspx
在DataContract 类型实现了IExtensibleDataObject接口之后,ExtensionDataObject特性就会被添加;这个特性在向前兼容的序列化中扮演重要的角色。WCF使用DataContractSerializer来为DataContract类型序列化/反序列化。当DataContractSerializer发现一个已经实现了IExtensibleDataObject接口的特定类型(被用来作为操作参数或者返回值),DataContractSerializer会存储所有在这个类型中没有对应定义的属性/字段的数据(在反序列化期间获得的消息流),将这些数据的定义保存在ExtensionDataObject属性中,以防止这些数据丢失。如果反序列化实例(一些未知数据存储在ExtensionDataObject中)序列化到消息后,DataContractSerializer将再次把ExtensionDataObject写入消息流。这确保了在使用旧版本类型定义的服务端/客户端的数据能在新版本的数据契约(DataContract)中被正确使用,避免了未知异常类型,不匹配或序列化异常。
以下修改后的数据类型可以被使用旧的数据定义的服务端/客户端消费,如前所述,没有同步DataContract类型定义:
[DataContract] public class FCQuestion : IExtensibleDataObject { [DataMember] public string Subject { get; set; } [DataMember] public string Answer { get; set; } [DataMember] public string Comment { get; set; } public ExtensionDataObject ExtensionData { get; set; } }
目前,当序列化/反序列化自定义数据类型时,使用IExtensibleDataObject接口可以使DataContractSerializer保存未知数据的属性/字段。然而,ExtensionDataObject特性对开发者是不透明的,即我们不能手动地读取存储在它里面的数据。假使我们想要手动地提取那些额外的未知属性/字段,我们可以考虑通过MessageInspector(消息检查器)或其他扩展点直接访问底层的SOAP消息。
关于MessageInspector,后续博文会进行介绍(原书在第9章)。
ps:因文章内容较长,将分为多篇博文。