Windows Communication Foundation 入门(Part One) 前言:WCF 是微软基于 SOA(Service Oriented Architecture)推出的.Net 平台下的框架 产品,它代表了软件架构设计与开发的一种发展方向,在微软的战略计划中也占有非常重要的地 位。了解和掌握 WCF,对于程序员特别是基于微软产品开发的程序员而言,是非常有必要的。 对于 WCF,笔者也是初窥门径,抱着学习的态度作这样的一个介绍。文中的内容主要参考了微 软官方的文档、资料,以及众多介绍 WCF 的技术资料。这些资料主要都是英文,不便于国内程 序员学习 WCF。虽然本人才疏学浅,却愿意作这样的介绍者。由于自己仅是一个初学者,英文 的功底也不够深厚,所以文中难免会有疏漏之处。同时,我也希望在文中尽量表达出自己的一些 心得与见解,这就不免增加了出现错误的可能性。此外,由于 WCF 至今仍未有正式的版本,文 中相关的技术描述以及代码会根据版本的不同而发生变化, 所以我也只能尽量对此给与一定的说 明。本文会是多篇文章拼凑在一起的系列,说是系列,但并没有严格的渐进关系,只是整体上希 望能有一个相对全面的 WCF 入门介绍。此外,笔者也希望能通过此文抛砖引玉,这样也能让我 的 WCF 学习之旅更轻松一点。 一、什么是 WCF? 根据微软官方的解释, WCF(之前的版本名为 “Indigo”)是使用托管代码建立和运行面向服 务(Service Oriented)应用程序的统一框架。它使得开发者能够建立一个跨平台的安全、可 信赖、事务性的解决方案,且能与已有系统兼容协作。 WCF 是微软分布式应用程序开发的集大 成者,它整合了 .Net 平台下所有的和分布式系统有关的技术,例如 .Net Remoting、ASMX、 WSE 和 MSMQ。以通信 (Communiation)范围而论,它可以跨进程、跨机器、跨子网、企业 网乃至于 Internet;以宿主程序而论,可以以 ASP.NET,EXE,WPF,Windows Forms, NT Service,COM+作为宿主 (Host)。WCF 可以支持的协议包括 TCP,HTTP,跨进程以及自 定义,安全模式则包括 SAML,Kerberos,X509,用户 /密码,自定义等多种标准与模式。也 就是说,在 WCF 框架下,开发基于 SOA 的分布式系统变得容易了,微软将所有与此相关的技 术要素都包含在内,掌握了 WCF,就相当于掌握了叩开 SOA 大门的钥匙。 WCF 是建立在 .Net Framework 2.0 基础之上的,正式的版本应该会作为 Windows Vista 的核心部分而 Release。然而,这并不代表 WCF 只能运行在 Windows Vista 下。只要安装了 WinFX Runtime Components,在 Windows XP 和 Windows 2003 操作系统下,仍然可以 使用。Visual Studio 2005 中并没有包含 WCF,但是当安装好了 WinFX Runtime Components 后,我们就可以在 Visual Studio 2005 环境下开发和创建 WCF 的程序了。 目前最新的 WCF 版本是 February 2006 CTP,下载页面是: http://www.microsoft.com/downloads/details.aspx?FamilyId=F51C4D96-9AEA-47 4F-86D3-172BFA3B828B&displaylang=en。使用 WCF 需要用到一些相关的工具,如 SvcUtil.exe,所以还需要下载 WinFX Runtime Components 的 SDK,其下载页面是: http://www.microsoft.com/downloads/details.aspx?FamilyId=9BE1FC7F-0542-47 F1-88DD-61E3EF88C402&displaylang=en。安装 SDK 可以选择网络安装或本地安装。如 果是本地安装,文件大小为 1.1G 左右,是 ISO 文件。安装了 SDK 后,在 program files 目 录下,有 microsoft SDK 目录。 WCF 是微软重点介绍的产品,因此也推出了专门的官方网站 (http://windowscommunication.net),该网站有最新的 WCF 新闻发布,以及介绍 WCF 的技术文档和样例代码。 二、WCF 的优势 在 David Chappell 所撰的《Introducing Windows Communication Foundation》一 文中,用了一个活鲜鲜的例子,来说明 WCF 的优势所在。假定我们要为一家汽车租赁公司开发 一个新的应用程序,用于租车预约服务。该租车预约服务会被多种应用程序访问,包括呼叫中心 (Call Center),基于 J2EE 的租车预约服务以及合作伙伴的应用程序( Partner Application), 如图所示: 呼叫中心运行在 Windows 平台下,是在 .Net Framework 下开发的应用程序,用户为公司 员工。由于该汽车租赁公司兼并了另外一家租赁公司,该公司原有的汽车预约服务应用程序是 J2EE 应用程序,运行在非 Windows 操作系统下。呼叫中心和已有的汽车预约应用程序都运行 在 Intranet 环境下。 合作伙伴的应用程序可能会运行在各种平台下, 这些合作伙伴包括旅行社、 航空公司等等,他们会通过 Internet 来访问汽车预约服务,实现对汽车的租用。 这样一个案例是一个典型的分布式应用系统。如果没有 WCF,利用 .Net 现有的技术应该如 何开发呢? 首先考虑呼叫中心,它和我们要开发的汽车预约服务一样,都是基于 .Net Framework 的应 用程序。呼叫中心对于系统的性能要求较高,在这样的前提下, .Net Remoting 是最佳的实现 技术。它能够高性能的实现 .Net 与.Net 之间的通信。 要实现与已有的 J2EE 汽车预约应用程序之间的通信,只有基于 SOAP 的 Web Service 可 以实现此种目的,它保证了跨平台的通信;而合作伙伴由于是通过 Internet 来访问,利用 ASP.Net Web Service,即 ASMX,也是较为合理的选择,它保证了跨网络的通信。由于涉及 到网络之间的通信,我们还要充分考虑通信的安全性,利用 WSE(Web Service Enhancements)可以为 ASMX 提供安全的保证。 一个好的系统除了要保证访问和管理的安全,高性能,同时还要保证系统的可信赖性。因此, 事务处理是企业应用必须考虑的因素, 对于汽车预约服务而言, 同样如此。 在.Net 中,Enterprise Service(COM+)提供了对事务的支持, 其中还包括分布式事务 (Distributed Transactions)。 不过对于 Enterprise Service 而言,它仅支持有限的几种通信协议。 如果还要考虑到异步调用、脱机连接、断点连接等功能,我们还需要应用 MSMQ(Microsoft Message Queuing),利用消息队列支持应用程序之间的消息传递。 如此看来,要建立一个好的汽车租赁预约服务系统,需要用到的 .Net 分布式技术,包括 .Net Remoting、Web Service,COM+等五种技术,这既不利于开发者的开发,也加大了程序的 维护难度和开发成本。正是因应于这样的缺陷, WCF 才会在.Net 2.0 中作为全新的分布式开发 技术被微软强势推出,它整合了上述所属的分布式技术,成为了理想的分布式开发的解决之道。 下图展示了 WCF 与之前的相关技术的比较: 从功能的角度来看, WCF 完全可以看作是 ASMX,.Net Remoting,Enterprise Service, WSE,MSMQ 等技术的并集。(注:这种说法仅仅是从功能的角度。事实上 WCF 远非简单的 并集这样简单,它是真正面向服务的产品,它已经改变了通常的开发模式。)因此,对于上述汽 车预约服务系统的例子,利用 WCF,就可以解决包括安全、可信赖、互操作、跨平台通信等等 需求。开发者再不用去分别了解 .Net Remoting,ASMX 等各种技术了。 概括地说, WCF 具有如下的优势: 1、统一性 前面已经叙述, WCF 是对于 ASMX,.Net Remoting,Enterprise Service,WSE,MSMQ 等技术的整合。 由于 WCF 完全是由托管代码编写, 因此开发 WCF 的应用程序与开发其它的 .Net 应用程序没有太大的区别,我们仍然可以像创建面向对象的应用程序那样,利用 WCF 来创建面 向服务的应用程序。 2、互操作性 由于 WCF 最基本的通信机制是 SOAP,这就保证了系统之间的互操作性,即使是运行不同 的上下文中。这种通信可以是基于 .Net 到.Net 间的通信,如下图所示: 可以跨进程、跨机器甚至于跨平台的通信,只要支持标准的 Web Service,例如 J2EE 应用 服务器(如 WebSphere,WebLogic)。应用程序可以运行在 Windows 操作系统下,也可以 运行在其他的操作系统,如 Sun Solaris,HP Unix,Linux 等等。如下图所示: 3、安全与可信赖 WS-Security,WS-Trust 和 WS-SecureConversation 均被添加到 SOAP 消息中,以用于 用户认证,数据完整性验证,数据隐私等多种安全因素。 在 SOAP 的 header 中增加了 WS-ReliableMessaging 允许可信赖的端对端通信。而建立在 WS-Coordination 和 WS-AtomicTransaction 之上的基于 SOAP 格式交换的信息, 则支持两 阶段的事务提交( two-phase commit transactions)。 上述的多种 WS-Policy 在 WCF 中都给与了支持。对于 Messaging 而言,SOAP 是 Web Service 的基本协议,它包含了消息头( header)和消息体 (body)。在消息头中,定义了 WS-Addressing 用于定位 SOAP 消息的地址信息,同时还包含了 MTOM(消息传输优化机制, Message Transmission Optimization Mechanism)。如图所示: 4、兼容性 WCF 充分的考虑到了与旧有系统的兼容性。安装 WCF 并不会影响原有的技术如 ASMX 和.Net Remoting。即使对于 WCF 和 ASMX 而言,虽然两者都使用了 SOAP,但基于 WCF 开发的应用程序,仍然可以直接与 ASMX 进行交互。 Windows Communication Foundation 入门(Part Two) 三、WCF 的技术要素 作为基于 SOA(Service Oriented Architecture)的一个框架产品, WCF 最重要的就是能够 快捷的创建一个服务( Service)。如下图所示,一个 WCF Service 由下面三部分构成: 1、Service Class:一个标记了 [ServiceContract]Attribute 的类,在其中可能包含多个方法。 除了标记了一些 WCF 特有的 Attribute 外,这个类与一般的类没有什么区别。 2、Host(宿主):可以是应用程序,进程如 Windows Service 等,它是 WCF Service 运行 的环境。 3、Endpoints:可以是一个,也可以是一组,它是 WCF 实现通信的核心要素。 WCF Service 由一个 Endpoints 集合组成,每个 Endpoint 就是用于通信的入口,客户端和服 务端通过 Endpoint 交换信息,如下图所示: 从图中我们可以看到一个 Endpoint 由三部分组成: Address,Binding,Contract。便于记忆, 我们往往将这三部分称为是 Endpoint 的 ABCs。 Address 是 Endpoint 的网络地址,它标记了消息发送的目的地。 Binding 描述的是如何发送 消息,例如消息发送的传输协议 (如 TCP,HTTP),安全(如 SSL,SOAP 消息安全) 。Contract 则描述的是消息所包含的内容,以及消息的组织和操作方式,例如是 one-way,duplex 和 request/reply。所以 Endpoint 中的 ABCs 分别代表的含义就是: where,how,what。当 WCF 发送消息时,通过 address 知道消息发送的地址,通过 binding 知道怎样来发送它,通 过 contract 则知道发送的消息是什么。 在 WCF 中,类 ServiceEndpoint 代表了一个 Endpoint,在类中包含的 EndpointAddress, Binding,ContractDescription 类型分别对应 Endpoint 的 Address,Binding,Contract, 如下图: EndpointAddress 类又包含 URI,Identity 和可选的 headers 集合组成,如下图: Endpoint 安全的唯一性识别通常是通过其 URI 的值,但为了避免一些特殊情况造成 URI 的重 复,又引入了 Identity 附加到 URI 上,保证了 Endpoint 地址的唯一性。至于可选的 AddressHeader 则提供了一些附加的信息,尤其是当多个 Endpoint 在使用同样的 URI 地址 信息时,AddressHeader 就非常必要了。 Binding 类包含 Name,Namespace 和 BindingElement 集合,如下图: Binding 的 Name 以及 Namespace 是服务元数据( service’s metadata)的唯一标识。 BindingElement 描述的是 WCF 通信时 binding 的方式。例如, SecurityBindingElement 表示 Endpoint 使用 SOAP 消息安全方式,而 ReliableSessionBindingElement 表示 Endpoint 利用可信赖消息确保消息的传送。 TcpTransportBindingElement 则表示 Endpoint 利用 TCP 作为通信的传输协议。每种 BindingElement 还有相应的属性值,进一步详细的描述 WCF 通信的方式。 BindingElement 的顺序也非常重要。 BindingElement 集合通常会创建一个用于通信的堆栈, 其顺序与 BindingElement 集合中元素顺序一致。 集合中最后一个 binding element 对应于通 信堆栈的底部,而集合中的第一个 binding element 则对应于堆栈的顶端。入消息流的方向是 从底部经过堆栈向上,而出消息流的方向则从顶端向下。因此, BindingElement 集合中的 binding element 顺序直接影响了通信堆栈处理消息的顺序。幸运的是, WCF 已经提供了一系 列预定义的 Binding,能够满足大多数情况,而不需要我们自定义 Binding,殚精竭虑地考虑 binding element 的顺序。 Contract 是一组操作( Operations)的集合,该操作定义了 Endpoint 通信的内容,每个 Operation 都是一个简单的消息交换( message exchange),例如 one-way 或者 request/reply 消息交换。 类 ContractDescription 用于描述 WCF 的 Contracts 以及它们的操作 operations。在 ContractDescription 类中,每个 Contract 的 operation 都有相对应的 OperationDescription,用于描述 operation 的类型, 例如是 one-way,还是 request/reply。 在 OperationDescription 中还包含了 MessageDecription 集合用于描述 message。 在 WCF 编程模型中, ContractDescription 通常是在定义 Contract 的接口或类中创建。对于 这个接口或类类型,标记以 ServiceContractAttribute,而其 Operation 方法则标记以 OperationContractAttribute。当然我们也可以不利用 CLR 的 attribute,而采用手工创建。 与 Binding 一样,每个 Contract 也包含有 Name 和 Namespace,用于在 Service 的元数据 中作为唯一性识别。 此外, Contract中还包含了ContractBehavior的集合, ContractBehavior 类型可以用于修改或扩展 contract 的行为。类 ContractDescription 的组成如下图所示: 正如在 ContractDescription 中包含的 IContractBehavior 一样, WCF 专门提供了行为 Behavior,它可以对客户端和服务端的一些功能进行修改或者扩展。例如 ServiceMetadataBehavior 用于控制 Service 是否发布元数据。相似的, security behavior 用于控制安全与授权, transaction behavior 则控制事务。 除了前面提到的 ContractBehavior,还包括 ServiceBehavior 和 ChannelBehaivor。 ServiceBehavior 实现了 IServiceBehavior 接口,ChannelBehaivor 则实现了 IChannleBehavior 接口。 由于 WCF 需要管理的是服务端与客户端的通信。对于服务端, WCF 提供了类 ServiceDescription 用于描述一个 WCF Service,;而针对客户端, WCF 管理的是发送消息 时需要使用到的通道 Channel,类 ChannelDescription 描述了这样的客户端通道。 ServiceDescription 类的组成如下图所示: 我们可以利用代码的方式创建 ServiceDescription 对象,也可以利用 WCF 的 Attribute,或 者使用工具 SvcUtil.exe。虽然可以显式的创建它,但通常情况下,它会作为运行中的 Service 一部分而被隐藏于后(我在后面会提到)。 ChannelDescription 类的组成与 ServiceDescription 大致相同,但它仅仅包含了一个 ServiceEndpoint,用于表示客户端通过通道通信的目标 Endpoint。当然,施加到 ChannelDescription 的 Behavior 也相应的为 IChannelBehavior 接口类型,如图所示: 定义一个 WCF Service 非常简单,以 Hello World 为例,定义的 Service 可能如下: using System.ServiceModel [ServiceContract] public class HelloWorld { [OperationContract] public void Hello() { Console.WriteLine(“Hello World!”); } } System.ServiceModel 是微软为 WCF 提供的一个新的类库,以用于面向服务的程序设计。在 开发 WCF 应用程序时,需要先添加对 System.ServiceModel 的引用。WCF 中的大部分类和 接口也都是在命名空间 System.ServiceModel 下。 我们为 HelloWorld 类标记了[ServiceContract],这就使得该类成为了一个 WCF Service, 而其中的方法 Hello()则因为标记了 [OperationContract],而成为该 Service 的一个 Operation。 不过 WCF 推荐的做法是将接口定义为一个 Service,这使得 WCF Service 具有更好的灵活性, 毕竟对于一个接口而言,可以在同时有多个类实现该接口,这也就意味着可以有多个 Service Contract 的实现。那么上面的例子就可以修改为: [ServiceContract] public interface IHello { [OperationContract] void Hello(); } 而类 HelloWorld 则实现该 IHello 接口: public class HelloWorld:IHello { public void Hello() { Console.WriteLine(“Hello World!”); } } 注意在实现了 IHello 接口的类 HelloWorld 中,不再需要在类和方法中标注 ServiceContractAttribute 和 OperationContractAttribute 了。 前面我已经提过,一个 WCF Service 必须有 host 作为它运行的环境。这个 host 可以是 ASP.Net,可以是 Windows Service,也可以是一个普通的应用程序,例如控制台程序。下面 就是一个 Host 的实现: using System.ServiceModel public class HostApp { static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(HelloWorld), new Uri(“http://localhost:8080/HelloService”)); host.AddServiceEndpoint(typeof(IHello), new BasicHttpBinding(),”Svc”); host.Open(); Console.WriteLine(“Start Your Service.”); Console.ReadKey(); host.Close(); } } 在这个 HostApp 中,我们为 HelloWorld 创建了一个 ServiceHost 对象。通过它就可以创建 WCF 运行时( Runtime),WCF Runtime 是一组负责接收和发送消息的对象。 ServiceHost 可以创建 SerivceDescription 对象,利用 SerivceDescription,SercieHost 为每一个 ServiceEndpoint 创建一个 EndpointListener。ServiceHost 的组成如下图: EndpointListener 侦听器包含了 listening address,message filtering 和 dispatch,它们 对应 ServiceEndpoint 中的 EndpointAddress,Contract 和 Binding。在 EndpointListener 中,还包含了一个 Channel Stack,专门负责发送和接收消息。 注意在创建 ServiceHost 时,传递的 type 类型参数,不能是 interface。因此,我在这里传入 的是 typeof(HelloWorld)。ServiceHost 类的 AddServiceEndpoint()方法实现了为 Host 添加 Endpoint 的功能,其参数正好是 Endpoint 的三部分: Address,Bingding 和 Contract。 (此时的 IHello 即为 ServiceContract,其方法 Hello 为 OperationContract)。 ServiceHost 的 Open()方法用于创建和打开 Service 运行时,而在程序结束后我又调用了 Close()方法,来关闭这个运行时。实际上以本例而言,该方法可以不调用,因为在应用程序结 束后,系统会自动关闭该 host。但作为一种良好的编程习惯, WCF 仍然要求显式调用 Close() 方法,因为 Service 运行时其本质是利用 Channel 来完成消息的传递,当打开一个 Service 运 行时的时候,系统会占用一个 Channel,调用完后,我们就需要释放对该通道的占用。当然我 们也可以用 using 语句来管理 ServiceHost 资源的释放。 定义好了一个 WCF Service,并将其运行在 Host 上后,如何实现它与客户端的通信呢?典型 的情况下,服务端与客户端均采用了 Web Service Description Language(WSDL),客户 端可以通过工具 SvcUtil.exe 生成对应于该 WCF Service 的 Proxy 代码,以完成之间的消息 传递,如图所示: SvcUtil.exe 是由 WinFx Runtime Component SDK 所提供的,如果安装 SDK 正确,可以 在其中找到该应用工具。生成客户端 Proxy 代码的方法很简单,首先需要运行服务端 Service。 然后再命令行模式下运行下面的命令: SvcUtil http://localhost:8080/HelloService 这样会在当前目录下产生两个文件 output.cs 和 output.config。前者最主要的就是包含了一 个实现了 IHello 接口的 Proxy 对象,这个代理对象名为 HelloProxy,代码生成的结果如下: [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")] public partial class HelloProxy : System.ServiceModel.ClientBase, IHello { public HelloProxy() { } public HelloProxy(string endpointConfigurationName) : base(endpointConfigurationName) { } public HelloProxy(string endpointConfigurationName, string remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public HelloProxy(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public HelloProxy(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public void Hello() { base.InnerProxy.Hello(); } } (注:本程序在 WinFx 2006 February CTP 版本下运行通过) 至于后者,则是 WCF Service 的配置信息,主要包含的是 Endpoint 中 Address,Binding 以 及 Contract 的配置(在后续文章我会详细介绍)。 现在客户端就可以直接使用 HelloProxy 对象,来完成与服务端的通信了: public class ClientApp { static void Main(string[] args) { using (HelloProxy proxy = new HelloProxy()) { proxy.Hello(); } Console.ReadKey(); } } 除了可以使用 SvcUtil 工具产生客户端代码,同样我们也可以利用代码的方式来完成客户端。客 户端在发送消息给服务端时,其通信的基础是 Service 的 Endpoint,WCF 提供了 System.ServiceModel.Description.ServiceEndpoint 类,通过创建它来实现两端的通信。 在前面,我还提到 “对于客户端而言, WCF 管理的是发送消息时需要使用到的通道 Channel”, 为此,WCF 提供了 ChannelFactory(其命名空间为 System.ServiceModel.Channel),专 门用于创建客户端运行时( runtime)。ChannelFactory 与 ServiceHost 相对应,它可以创 建 ChannelDescription 对象。与服务端 ServiceHost 不同的是,客户端并不需要侦听器,因 为客户端往往是建立连接的 “发起方 ”,并不需要侦听进来的连接。 因此客户端的 Channel Stack 会由 ChannelDescription 创建。 ChannelFactory 和 ServiceHost 都具有 Channel Stack,而服务端与客户端的通信又是通过 channel 来完成,这就意味着,利用 ChannelFactory,客户端可以发送消息到服务端。而客 户端本身并不存在 Service 对象,因此该 Service 的 Proxy,是可以通过 Channel 来得到的。 所以客户端的代码可以修改如下: using System.ServiceModel; using System.ServiceModel.Description; using System.ServiceModel.Channel public class ClientApp { static void Main(string[] args) { ServiceEndpoint httpEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IHello)), new BasicHttpBinding(), new EndpointAddress(“http://localhost:8080/HelloService/Svc”)); using (ChannelFactory factory = new ChannelFactory(httpEndPoint)) { //创建 IHello 服务的代理对象; IHello service = factory.CreateChannel(); service.Hello(); } Console.ReadKey(); } } (注:本程序在 WinFx 2006 February CTP 版本下运行通过) 对于上面的代码,我们有两点需要注意: 1、采用这种方式,前提条件是客户端能够访问 IHello 接口。这也印证了之前我所叙述的最好 使用 interface 来定义 Service 的好处。此外,为了保证部署的方便, 有关 Service 的 interface 最好单独编译为一个程序集,便于更好的部署到客户端。 2、客户端必须知道服务端 binding 的方式以及 address。 对于服务端而言,我们也可以直接在浏览器中打开该 Service,在地址栏中输入 http://localhost:8080/HelloService,如下图: 点击链接: http://localhost:8080/HelloService?wsdl,我们可以直接看到 HelloService 的 WSDL。注意到在这里我并没有使用 IIS,实际上 WCF 内建了对 httpsys 的集成,允许任何 应用程序自动成为 HTTP listener。 Windows Communication Foundation 入门(Part Three) 《Windows Communication Foundation 之旅》系列之三 示例代码下载: DuplexSample.rar 四、Service Contract 编程模型 在 Part Two 中,我以“Hello World”为例讲解了如何定义一个 Service。其核心就是为接口或 类施加 ServiceContractAttribute,为方法施加 OperationContractAttribute。在 Service 的方法中,可以接受多个参数,也可以有返回类型,只要这些数据类型能够被序列化。这样一种 方式通常被称为本地对象,远程过程调用( local-object, Remoting-Procedure-Call)方式, 它非常利于开发人员快速地进行 Service 的开发。 在 Service Contract 编程模型中,还有一种方式是基于 Message Contract 的。服务的方法 最多只能有一个参数,以及一个返回值,且它们的数据类型是通过 Message Contract 自定义 的消息类型。在自定义消息中,可以为消息定义详细的 Header 和 Body,使得对消息的交换更 加灵活,也更利于对消息的控制。 一个有趣的话题是当我们定义一个 Service 时,如果一个 private 方法被施加了 OperationContractAttribute,那么对于客户端而言,这个方法是可以被调用的。这似乎与 private 对于对象封装的意义有矛盾。但是这样的规定是有其现实意义的,因为对于一个服务而 言,服务端和客户端的需求往往会不一致。在服务端,该服务对象即使允许被远程调用,但本地 调用却可能会因情况而异。如下面的服务定义: [ServiceContract] public class BookTicket { [OperationContract] public bool Check(Ticket ticket) { bool flag; //logic to check whether the ticket is none; return flag; } [OperationContract] private bool Book(Ticket ticket) { //logic to book the ticket } } 在服务类 BookTicket 中,方法 Check 和 Book 都是服务方法,但后者被定义成为 private 方 法。为什么呢?因为对于客户而言,首先会检查是否还有电影票,然而再预定该电影票。也就是 说这两项功能都是面向客户的服务,会被远程调用。对于 Check 方法,除了远程客户会调用该 方法之外,还有可能被查询电影票、预定电影票、出售电影票等业务逻辑所调用。而 Book 方法, 则只针对远程客户,只可能被远程调用。为了保证该方法的安全,将其设置为 private,使得本 地对象不至于调用它。 因此在 WCF 中,一个方法是否应该被设置为服务方法,以及应该设置为 public 还是 private, 都需要根据具体的业务逻辑来判断。如果涉及到私有的服务方法较多,一种好的方法是利用设计 模式的 Façade 模式,将这些方法组合起来。而这些方法的真实逻辑,可能会散放到各自的本地 对象中,对于这些本地对象,也可以给与一定的访问限制,如下面的代码所示: internal class BusinessObjA { internal void FooA(){} } internal class BusinessObjB { internal void FooB(){} } internal class BusinessObjC { internal void FooC(){} } [ServiceContract] internal class Façade { private BusinessObjA objA = new BusinessObjA(); private BusinessObjB objB = new BusinessObjB(); private BusinessObjC objC = new BusinessObjC(); [OperationContract] private void SvcA() { objA.FooA(); } [OperationContract] private void SvcB() { objB.FooB(); } [OperationContract] private void SvcC() { objC.FooC(); } } 方法 FooA,FooB,FooC 作为 internal 方法,拒绝被程序集外的本地对象调用,但 SvcA, SvcB 和 SvcC 方法,却可以被远程对象所调用。我们甚至可以将 BusinessObjA, BusinessObjB 等类定义为 Façade 类的嵌套类。采用这样的方法,有利于这些特殊的服务方 法,被远程客户更方便的调用。 定义一个 Service,最常见的还是显式地将接口定义为 Service。这样的方式使得服务的定义更 加灵活,这一点,我已在 Part Two 中有过描述。当然,采用这种方式,就不存在前面所述的私 有方法成为服务方法的形式了,因为在一个接口定义中,所有方法都是 public 的。 另外一个话题是有关 “服务接口的继承 ”。一个被标记了 [ServiceContract]的接口,在其继承链 上,允许具有多个同样标记了 [ServiceContract]的接口。 对接口内定义的 OperationContract 方法,则是根据 “聚合”的原则,如下的代码所示: [ServiceContract] public interface IOne { [OperationContract(IsOneWay=true)] void A(); } [ServiceContract] public interface ITwo { [OperationContract] void B(); } [ServiceContract] public interface IOneTwo : IOne, ITwo { [OperationContract] void C(); } 在这个例子中,接口 IOneTwo 继承了接口 IOne 和 ITwo。此时服务 IOneTwo 暴露的服务方 法应该为方法 A、B 和 C。 然而当我们采用 Duplex 消息交换模式(文章后面会详细介绍 Duplex)时,对于服务接口的回 调接口在接口继承上有一定的限制。 WCF 要求服务接口 IB 在继承另一个服务接口 IA 时,IB 的回调接口 IBCallBack 必须同时继承 IACallBack,否则会抛出 InvalidContractException 异常。正确的定义如下所示: [ServiceContract(CallbackContract = IACallback)] interface IA {} interface IACallback {} [ServiceContract(CallbackContract = IBCallback)] interface IB : IA {} interface IBCallback : IACallback {} 五、消息交换模式( Message Exchange Patterns,MEPS) 在 WCF 中,服务端与客户端之间消息的交换共有三种模式: Request/Reply,One-Way, Duplex。 1、Request/Reply 这是默认的一种消息交换模式,客户端调用服务方法发出请求( Request),服务端收到请求 后,进行相应的操作,然后返回一个结果值( Reply)。 如果没有其它特别的设置,一个方法如果标记了 OperationContract,则该方法的消息交换模 式就是采用的 Request/Reply 方式,即使它的返回值是 void。当然, 我们也可以将 IsOneWay 设置为 false,这也是默认的设置。如下的代码所示: [ServiceContract] public interface ICalculator { [OperationContract] int Add(int a, int b); [OperationContract] int Subtract(int a, int b); } 2、One-Way 如果消息交换模式为 One-Way,则表明客户端与服务端之间只有请求,没有响应。即使响应信 息被发出,该响应信息也会被忽略。这种方式类似于消息的通知或者广播。当一个服务方法被设 置为 One-Way 时,如果该方法有返回值,会抛出 InvalidOperationException 异常。 要将服务方法设置为 One-Way 非常简单,只需要将 OperationContractAttribute 的属性 IsOneWay 设置为 true 就可以了,如下的代码所示: public class Radio { [OperationContract(IsOneWay=true)] private void BroadCast(); } 3、Duplex Duplex 消息交换模式具有客户端与服务端双向通信的功能,同时它的实现还可以使消息交换具 有异步回调的作用。 要实现消息交换的 Duplex,相对比较复杂。它需要定义两个接口,其中服务接口用于客户端向 服务端发送消息,而回调接口则是从服务端返回消息给客户端,它是通过回调的方式来完成的。 接口定义如下: 服务接口: [ServiceContract(Namespace = "http://microsoft.servicemodel.samples/", Session = true, CallbackContract=typeof(ICalculatorDuplexCallback))] public interface ICalculatorDuplex { [OperationContract(IsOneWay=true)] void Clear(); [OperationContract(IsOneWay = true)] void AddTo(double n); [OperationContract(IsOneWay = true)] void SubtractFrom(double n); [OperationContract(IsOneWay = true)] void MultiplyBy(double n); [OperationContract(IsOneWay = true)] void DivideBy(double n); } 回调接口: public interface ICalculatorDuplexCallback { [OperationContract(IsOneWay = true)] void Equals(double result); [OperationContract(IsOneWay = true)] void Equation(string equation); } 注意在接口定义中,每个服务方法的消息转换模式均设置为 One-Way。此外,回调接口是被本 地调用,因此不需要定义 [ServiceContract]。在服务接口中,需要设置 ServiceContractAttribute 的 CallbackContract 属性,使其指向回调接口的类型 type。 对于实现服务的类,实例化模式( InstanceContextMode)究竟是采用 PerSession 方式,还 是 PerCall 方式,应根据该服务对象是否需要保存状态来决定。如果是 PerSession,则服务对 象的生命周期是存活于一个会话期间。而 PerCall 方式下,服务对象是在方法被调用时创建,结 束后即被销毁。然而在 Duplex 模式下,不能使用 Single 方式,否则会导致异常抛出。本例的 实现如下: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class CalculatorService : ICalculatorDuplex { double result; string equation; ICalculatorDuplexCallback callback = null; public CalculatorService() { result = 0.0D; equation = result.ToString(); callback = OperationContext.Current. GetCallbackChannel(); } public void AddTo(double n) { result += n; equation += " + " + n.ToString(); callback.Equals(result); } // Other code not shown. } 在类 CalculatorService 中,回调接口对象 callback 通过 OperationContext.Current.GetCallbackChannel<>()获取。 然后在服务方法例如 AddTo() 中,通过调用该回调对象的方法,完成服务端向客户端返回消息的功能。 在使用 Duplex 时,Contract 使用的 Binding 应该是系统提供的 WSDualHttpBinding,如果 使用 BasicHttpBinding,会出现错误。因此 Host 程序应该如下所示: public static void Main(string[] args) { Uri uri = new Uri("http://localhost:8080/servicemodelsamples"); using (ServiceHost host = new ServiceHost(typeof(CalculatorService), uri)) { host.AddServiceEndpoint(typeof(ICalculatorDuplex),new WSDualHttpBinding(),"service.svc"); host.Open(); Console.WriteLine("Press any key to quit service."); Console.ReadKey(); } } 如果是使用配置文件,也应作相应的修改,如本例: 当服务端将信息回送到客户端后,对消息的处理是由回调对象来处理的,所以回调对象的实现应 该是在客户端完成,如下所示的代码应该是在客户端中: public class CallbackHandler : ICalculatorDuplexCallback { public void Equals(double result) { Console.WriteLine("Equals({0})", result); } public void Equation(string equation) { Console.WriteLine("Equation({0})", equation); } } 客户端调用服务对象相应的为: class Client { static void Main() { // Construct InstanceContext to handle messages on // callback interface. InstanceContext site = new InstanceContext(new CallbackHandler()); // Create a proxy with given client endpoint configuration. using (CalculatorDuplexProxy proxy = new CalculatorDuplexProxy(site, "default")) { double value = 100.00D; proxy.AddTo(value); value = 50.00D; proxy.SubtractFrom(value); // Other code not shown. // Wait for callback messages to complete before // closing. System.Threading.Thread.Sleep(500); // Close the proxy. proxy.Close(); } } } 注意在 Duplex 中,会话创建的时机并不是客户端创建 Proxy 实例的时候,而是当服务对象的 方法被第一次调用时, 会话方才建立, 此时服务对象会在方法调用之前被实例化, 直至会话结束, 服务对象都是存在的。 以上的代码例子在 WinFX 的 SDK Sample 中可以找到。不过该例子并不能直接反映出 Duplex 功能。通过前面的介绍,我们知道 Duplex 具有客户端与服务端双向通信的功能,同时它的实现 还可以使消息交换具有异步回调的作用。因此,我分别实现了两个实例来展现 Duplex 在两方面 的作用。 (1)客户端与服务端双向通信功能 ——ChatDuplexWin 实例说明:一个类似于聊天室的小程序。利用 Duplex 支持客户端与服务端通信的特点,实现了 客户端与服务端聊天的功能。 服务接口和回调接口的定义如下: [ServiceContract(Namespace = "http://www.brucezhang.com/WCF/Samples/ChatDuplex", Session = true, CallbackContract=typeof(IChatDuplexCallback))] public interface IChatDuplex { [OperationContract(IsOneWay=true)] void Request(string cltMsg); [OperationContract(IsOneWay = true)] void Start(); } public interface IChatDuplexCallback { [OperationContract(IsOneWay=true)] void Reply(string srvMsg); } 很明显,Request 方法的功能为客户端向服务端发送消息, Reply 方法则使服务端回送消息给 客户端。服务接口 IChatDuplex 中的 Start()方法,用于显示的建立一个会话,因为在这个方 法中,我需要直接获取 callback 对象,使得服务端不必等待客户端先发送消息,而是可以利用 callback 对象主动先向客户端发送消息,从而实现聊天功能。 实现类的代码如下: [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)] public class ChatDuplex:IChatDuplex { public ChatDuplex() { m_callback = OperationContext.Current.GetCallbackChannel(); } private IChatDuplexCallback m_callback = null; public void Request(string cltMsg) { ChatRoomUtil.MainForm.FillListBox(string.Format("Client:{0}", cltMsg)); } public void Start() { ChatRoomUtil.MainForm.SetIIMDuplexCallback(m_callback); } } 因为我要求在服务端界面中,能够将客户端发送来的消息显示在主窗体界面中。所以利用了全局 变量 MainForm,用来保存主窗体对象: public static class ChatRoomUtil { public static ServerForm MainForm = new ServerForm(); } 而在服务端程序运行时, Application 运行的主窗口也为该全局变量: Application.Run(ChatRoomUtil.MainForm); 要实现聊天功能,最大的障碍是当服务端收到客户端消息时,不能立即 Reply 消息,而应等待 服务端用户输入回送的消息内容, 方可以 Reply。也即是说, 当客户端调用服务对象的 Request 方法时, 不能直接调用 callback 对象。 因此我利用 Start()方法,将服务对象中获得的 callback 对象传递到主窗体对象中。这样, callback 对象就可以留待服务端发送消息时调用了: public partial class ServerForm : Form { private IChatDuplexCallback m_callback; private void btnSend_Click(object sender, EventArgs e) { if (txtMessage.Text != string.Empty) { lbMessage.Items.Add(string.Format("Server:{0}", txtMessage.Text)); if (m_callback != null) { m_callback.Reply(txtMessage.Text); } txtMessage.Text = string.Empty; } } public void FillListBox(string message) { lbMessage.Items.Add(message); } public void SetIIMDuplexCallback(IChatDuplexCallback callback) { m_callback = callback; } //Other code not shown; } 对于客户端的实现,相对简单,需要注意的是回调接口的实现: public class ChatDuplexCallbackHandler:IChatDuplexCallback { public ChatDuplexCallbackHandler(ListBox listBox) { m_listBox = listBox; } private ListBox m_listBox; public void Reply(string srvMsg) { m_listBox.Items.Add(string.Format("Server:{0}", srvMsg)); } } 由于我自定义了该对象的构造函数,所以在实利化 proxy 时会有稍微区别: InstanceContext site = new InstanceContext(new ChatDuplexCallbackHandler(this.lbMessage)); proxy = new ChatDuplexProxy(site); proxy.Start(); 通过 proxy 对象的 Start()方法,使得我们在建立 proxy 对象开始,就创建了会话,从而使得 服务对象被实例化,从而得以运行下面的这行代码: m_callback = OperationContext.Current.GetCallbackChannel(); 也就是说,在 proxy 对象建立之后,服务端就已经获得 callback 对象了,这样就可以保证服务 端能够先向客户端发送消息而不会因为 callback 为 null,导致错误的发生。 (2)消息交换的异步回调功能 ——AsyncDuplexWin 实例说明:本实例比较简单,只是为了验证当回调对象被调用时,客户端是否可以被异步运行。 调用服务对象时,服务端会进行一个累加运算。在运算未完成之前,客户端会执行显示累加数字 的任务,当服务端运算结束后,只要客户端程序的线程处于 Sleep 状态,该回调对象就会被调 用,然后根据用户选择是否再继续运行剩下的任务。本例中服务端为控制台应用程序,客户端则 为 Windows 应用程序。 例子中的接口定义非常简单,不再赘述,而实现类的代码如下: public class SumDuplex:ISumDuplex { public SumDuplex() { callback = OperationContext.Current.GetCallbackChannel(); } private ISumDuplexCallback callback = null; #region ISumDuplex Members public void Sum(int seed) { int result = 0; for (int i = 1; i < = seed; i++) { Thread.Sleep(10); Console.WriteLine("now at {0}",i); result += i; } callback.Equals(result); } #endregion } 很显然,当客户端调用该服务对象时,会在服务端的控制台上打印出迭代值。 由于客户端需要在 callback 调用时,停止对当前任务的运行,所以需要用到多线程机制: public delegate void DoWorkDelegate(); public partial class ClientForm : Form { public ClientForm() { InitializeComponent(); InstanceContext site = new InstanceContext(new SumDuplexCallbackHandler(this.lbMessage)); proxy = new SumDuplexProxy(site); } private SumDuplexProxy proxy; private Thread thread = null; private DoWorkDelegate del = null; private int counter = 0; private void btnStart_Click(object sender, EventArgs e) { proxy.Sum(100); thread = new Thread(new ThreadStart(delegate() { while (true) { if (ClientUtil.IsCompleted) { if (MessageBox.Show("Game over,Exit?", "Notify", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { break; } } if (counter > 10000) { break; } if (del != null) { del(); } Thread.Sleep(50); } } )); del += new DoWorkDelegate(DoWork); thread.Start(); } private void DoWork() { if (lbMessage.InvokeRequired) { this.Invoke(new DoWorkDelegate(DoWork)); } else { lbMessage.Items.Add(counter); lbMessage.Refresh(); counter++; } } private void ClientForm_FormClosing(object sender, FormClosingEventArgs e) { if (thread != null) { thread.Abort(); } } } 因为需要在多线程中对 ListBox 控件的 items 进行修改,由于该控件不是线程安全的,所以应 使用该控件的 InvokeRequired 属性。此外,在线程启动时的匿名方法中,利用 while(true) 控制当前线程的运行,并利用全局变量 ClientUtil.IsCompleted 判断回调对象是否被调用,如 果被调用了,则会弹出对话框,选择是否退出当前任务。这里所谓的当前任务实际上就是调用 DoWork 方法,向 ListBox 控件的 items 中不断添加累加的 counter 值。注意客户端的回调对 象实现如下: class SumDuplexCallbackHandler:ISumDuplexCallback { public SumDuplexCallbackHandler(ListBox listBox) { m_listBox = listBox; } private ListBox m_listBox; #region ISumDuplexCallback Members public void Equals(int result) { ClientUtil.IsCompleted = true; m_listBox.Items.Add(string.Format("The result is:{0}", result)); m_listBox.Refresh(); } #endregion } 当客户端点击 Start 按钮,调用服务对象的 Sum 方法后,在服务端会显示迭代值,而客户端也 开始执行自己的任务,向 ListBox 控件中添加累加数。一旦服务端运算完毕,就将运算结果通 过回调对象传送到客户端,全局变量 ClientUtil.IsCompleted 将被设置为 true。如果添加累 加值的线程处于 sleep 状态,系统就会将结果值添加到 ListBox 控件中,同时会弹出对话框, 决定是否继续剩下的任务。 注:本文示例的代码和实例均在 Feb 2006 CTP 版本下运行。 Windows Communication Foundation 之旅(Part Four) 《Windows Communication Foundation 之旅》系列之四 六、定义 DataContract 我在介绍如何定义一个 ServiceContract 时,举了这样的一个例子,代码如下: [ServiceContract] public class BookTicket { [OperationContract] public bool Check(Ticket ticket) { bool flag; //logic to check whether the ticket is none; return flag; } [OperationContract] private bool Book(Ticket ticket) { //logic to book the ticket } } 在 Service 类 BookTicket 中,两个服务方法 Check 和 Book 的参数均为 Ticket 类型。这个 类型是自定义类型,根据 WCF 的要求,该类型必须支持序列化的操作,方才可以在服务方法中 作为消息被传递。 在.Net 中,除了基本类型如 int,long,double,以及枚举类型和 String 类型外,一个自定义 的类型如要支持序列化操作,应该标记该类型为 [Serializable],或者使该类型实现 ISerializable接口。而在WCF中,推荐的一种方式是为这些类型标记 DataContractAttribute。 方法如下: [DataContract] public class Ticket { private string m_movieName; [DataMember] public int SeatNo; [DataMember] public string MovieName { get {return m_movieName;} set {m_movieName = value;} } [DataMember] private DateTime Time; } 其中,[DataMember]是针对 DataContract 类的成员所标记的 Attribute。与服务类中的 OperationContractAttribute 相同, DataMemberAttribute 与对象的访问限制修饰符 (public,internal,private 等)没有直接关系。即使该成员为 private,只要标记了 [DataMember],仍然可以被序列化。虽然 DataMemberAttribute 可以被施加给类型的字段 和属性,但如果被施加到 static 成员时, WCF 会忽略该 DataMemberAttribute。 当我们为一个类型标注 DataContractAttribute 时,只有被显式标注了 DataMemberAttribute 的成员方才支持序列化操作。这一点与 SerializableAttribute 大相径 庭。一个被标记了 SerializableAttribute 的类型,在默认情况下, 其内部的成员, 不管是 public 还是 private 都支持序列化,除非是那些被施加了 NonSerializedAttribute 的成员。 DataContractAttribute 采用这种显式标注法,使得我们更加专注于服务消息的定义,只有需 要被传递的服务消息成员,方才被标注 DataMemberAttribute。 如果 DataContract 类中的 DataMember 成员包含了泛型,那么泛型类型参数必须支持序列 化,如下代码所示: [DataContract] public class MyGeneric { [DataMember] T theData; } 在类 MyGeneric 中,泛型参数 T 必须支持序列化。如实例化该对象: MyGeneric intObject = new MyGeneric(); MyGeneric customObject = new MyGeneric(); 对象intObject 由于传入的泛型参数为 int 基本类型, 因此可以被序列化; 而对象 customObject 是否能被序列化,则要看传入的泛型参数 CustomType 类型是否支持序列化。 DataContract 以 Namespace 和 Name 来唯一标识,我们可以在 DataContractAttribute 的 Namespace 属性、Name 属性中进行设置。如未设置 DataContract 的 Name 属性,则默 认的名字为定义的类型名。 DataMember 也可以通过设置 Name 属性,默认的名字为定义的 成员名。如下代码所示: namespace MyCompany.OrderProc { [DataContract] public class PurchaseOrder { // DataMember 名字为默认的 Amount; [DataMember] public double Amount; // Name 属性将重写默认的名字 Ship_to,此时 DataMember 名为 Address; [DataMember(Name = "Address")] public string Ship_to; } //Namespace 为默认值: // http://schemas.datacontract.org/2004/07/MyCompany.OrderProc //此时其名为 PurchaseOrder 而非 MyInvoice [DataContract(Name = "PurchaseOrder")] public class MyInvoice { // Code not shown. } // 其名为 Payment 而非 MyPayment // Namespace 被设置为 http://schemas.example.com [DataContract(Name = "Payment", Namespace = "http://schemas.example.com")] public class MyPayment { // Code not shown. } } // 重写 MyCorp.CRM 下的所有 DataContract 的 Namespace [assembly:ContractNamespace( ClrNamespace = "MyCorp.CRM", Namespace= "http://schemas.example.com/crm")] namespace MyCorp.CRM { // 此时 Namespace 被设置为 http://schemas.example.com/crm. // 名字仍然为默认值 Customer [DataContract] public class Customer { // Code not shown. } } 由于 DataContract 将被序列化以及反序列化,因此类型中成员的顺序也相当重要,在 DataMemberAttribute 中,提供了 Order 属性,用以设置成员的顺序。在 WCF 中对成员的 序列化顺序规定如下: 1、默认的顺序依照字母顺序; 2、如成员均通过 Order 属性指定了顺序,且顺序值相同,则以字母顺序; 3、未指定 Order 属性的成员顺序在指定了 Order 顺序之前; 4、如果 DataContract 处于继承体系中,则不管子类中指定的 Order 值如何,父类的成员顺 序优先。 下面的代码很好的说明了 DataMember 的顺序: [DataContract] public class BaseType { [DataMember] public string zebra; } [DataContract] public class DerivedType : BaseType { [DataMember(Order = 0)] public string bird; [DataMember(Order = 1)] public string parrot; [DataMember] public string dog; [DataMember(Order = 3)] public string antelope; [DataMember] public string cat; [DataMember(Order = 1)] public string albatross; } 序列化后的 XML 内容如下: <DerivedType> <zebra/> <cat/> <dog/> <bird/> <albatross/> <parrot/> <antelope/> </DerivedType> 因为成员 zebra 为父类成员,首先其顺序在最前面。 cat 和 dog 未指定 Order,故在指定了 Order 的其它成员之前, 两者又按照字母顺序排列。 parrot 和 albatross 均指定 Order 值为 1, 因此也按照字母顺序排列在 Order 值为 0 的 bird 之后。 判断两个 DataContract 是否相同,应该根据 DataContract 的 Namespace 和 Name,以及 DataMember 的 Name 和 Order 来综合判断。例如下面代码所示的类 Customer 和 Person 其实是同一个 DataContract: [DataContract] public class Customer { [DataMember] public string fullName; [DataMember] public string telephoneNumber; } [DataContract(Name=”Customer”)] public class Person { [DataMember(Name = "fullName")] private string nameOfPerson; private string address; [DataMember(Name= "telephoneNumber")] private string phoneNumber; } 再例如下面代码所示的类 Coords1、Coords2、Coords3 也是相同的 DataContract,而类 Coords4 则因为顺序不同,因而与前面三个类是不同的: [DataContract(Name= "Coordinates")] public class Coords1 { [DataMember] public int X; [DataMember] public int Y; } [DataContract(Name= "Coordinates")] public class Coords2 { [DataMember] public int Y; [DataMember] public int X; } [DataContract(Name= "Coordinates")] public class Coords3 { [DataMember(Order=2)] public int Y; [DataMember(Order=1)] public int X; } [DataContract(Name= "Coordinates")] public class Coords4 { [DataMember(Order=1)] public int Y; [DataMember(Order=2)] public int X; } 当 DataContract 处于继承体系时,还需要注意的是对象的 “多态”问题。如果在服务端与客户 端之间要传递消息,经常会涉及到类型的动态绑定。根据规定,如果消息的类型是子类类型,那 么发送消息一方就不能传递基类类型。相反,如果消息类型是父类类型,那么发送消息一方就可 以是父类本身或者其子类。从这一点来看, WCF 的规定是与面向对象思想并行不悖的。但是可 能存在的问题是,当消息类型定义为父类类型,而发送消息一方传递其子类时,服务端有可能对 该子类类型处于 “未知”状态,从而不能正常地反序列化。所以, WCF 为 DataContract 提供了 KnownTypeAttribute,通过设置它来告知服务端可能存在的动态绑定类类型。 举例来说,如果我们定义了这样三个类: [DataContract] public class Shape { } [DataContract(Name = "Circle")] public class CircleType : Shape { } [DataContract(Name = "Triangle")] public class TriangleType : Shape { } 然后在类 CompanyLogo 中定义 Shape 类型的字段,如下所示: [DataContract] public class CompanyLogo { [DataMember] private Shape ShapeOfLogo; [DataMember] private int ColorOfLogo; } 此时的 CompanyLogo 类由于正确的设置了 [DataContract]和[DataMember],而 Shape 类型也是支持序列化的, 因此该类型是可以被序列化的。 然而一旦客户端在调用 CompanyLogo 类型的对象时,为字段 ShapeOfLogo 设置其值为 CircleType 或 TriangleType 类型的对象, 就会发生反序列化错误,因为服务端并不知道 CircleType 或 TriangleType 类型,从而无法进 行正确的匹配。所以上述的 CompanyLogo 类的定义应修改如下: [DataContract] [KnownType(typeof(CircleType))] [KnownType(typeof(TriangleType))] public class CompanyLogo { [DataMember] private Shape ShapeOfLogo; [DataMember] private int ColorOfLogo; } 类的继承如此,接口的实现也是同样的道理,如下例所示: public interface ICustomerInfo { string ReturnCustomerName(); } [DataContract(Name = "Customer")] public class CustomerType : ICustomerInfo { public string ReturnCustomerName() { return "no name"; } } [DataContract] [KnownType(typeof(CustomerType))] public class PurchaseOrder { [DataMember] ICustomerInfo buyer; [DataMember] int amount; } 由于 PurchaseOrder 中定义了 ICustomerInfo 接口类型的字段,如要该类能被正确的反序列 化,就必须为类 PurchaseOrder 加上[KnownType(typeof(CustomerType))]的标注。 对于集合类型也有相似的规定。例如 Hashtable 类型,其内存储的均为 object 对象,但实际 设置的值可能是一些自定义类型,此时也许要通过 KnownType 进行标注。例如在类 LibraryCatalog 中,定义了 Hashtable 类型的字段 theCatalog。该字段可能会设置为 Book 类型和 Magazine 类型,假定 Book 类型和 Magazine 类型均被定义为 DataContract,则类 LibraryCatalog 的正确定义应如下所示: [DataContract] [KnownType(typeof(Book))] [KnownType(typeof(Magazine))] public class LibraryCatalog { [DataMember] System.Collections.Hashtable theCatalog; } 如果在一个 DataContract 中,定义一个 object 类型的字段。由于 object 类型是所有类型的 父类,所以需要我们利用 KnownType 标明客户端允许设置的类型。例如类 MathOperationData: [DataContract] [KnownType(typeof(int[]))] public class MathOperationData { private object numberValue; [DataMember] public object Numbers { get { return numberValue; } set { numberValue = value; } } //[DataMember] //public Operation Operation; } 属性 Numbers 其类型为 object,而 KnownType 设置的类型是 int[],因此可以接受的类型 就包括:整型,整型数组以及 List 类型。如下的调用都是正确的: static void Run() { MathOperationData md = new MathOperationData(); int a = 100; md.Numbers = a; int[] b = new int[100]; md.Numbers = b; List c = new List(); md.Numbers = c; } 但如果设置 Number 属性为 ArrayList,即使该 ArrayList 对象中元素均为 int 对象,也是错误 的: static void Run() { MathOperationData md = new MathOperationData(); ArrayList d = new ArrayList(); md.Numbers = d; } 一旦一个 DataContract 类型标注了 KnownTypeAttribute,则该 Attribute 的作用域可以施 加到其子类中,如下所示: [DataContract] [KnownType(typeof(CircleType))] [KnownType(typeof(TriangleType))] public class MyDrawing { [DataMember] private object Shape; [DataMember] private int Color; } [DataContract] public class DoubleDrawing : MyDrawing { [DataMember] private object additionalShape; } 虽然 DoubleDrawing 没有标注 KnowTypeAttribute,但其字段 additionalShape 仍然可以 被设置为 CircleType 类型或 TriangleType 类型,因为其父类已经被设置为 KnowTypeAttribute。 注:KnowTypeAttribute 可以标注类和结构,但不能标注接口。此外, DataContract 同样不 能标注接口,仅可以标注类、结构和枚举。要使用 DataContractAttribute、 DataMemberAttribute 和 KnownTypeAttribute,需要添加 WinFx 版本的 System.Runtime.Serialization 程序集的引用。
还剩50页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

s2333420

贡献于2015-09-06

下载需要 8 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf