欢迎来到我的地盘:今天是

若得山花插满头,莫问奴归处!

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
前言:WCF是微软基于SOA(Service Oriented Architecture)推出的.Net平台下的框架产品,它代表了软件架构设计与开发的一种发展方向,在微软的战略计划中也占有非常重要的地位。了解和掌握WCF,对于程序员特别是基于微软产品开发的程序员而言,是非常有必要的。

一、什么是WCF?



根据微软官方的解释,WCF是使用托管代码建立和运行面向服务(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基础之上的,包含在.NET 3.0/3.5当中。2005中并没有包含WCF,但是当安装好了WinFX Runtime Components后,我们就可以在Visual Studio 2005环境下开发和创建WCF的程序了。
WCF是微软重点介绍的产品,因此也推出了专门的官方网站(http://windowscommunication.net/),该网站有最新的WCF新闻发布,以及介绍WCF的技术文档和样例代码。

二、WCF的优势



在David Chappell所撰的《Introducing Windows Communication Foundation》一文中,用了一个活鲜鲜的例子,来说明WCF的优势所在。假定我们要为一家汽车租赁公司开发一个新的应用程序,用于租车预约服务。该租车预约服务会被多种应用程序访问,包括呼叫中心(Call Center),基于J2EE的租车预约服务以及合作伙伴的应用程序(Partner Application),如图所示:

uploads/200708/06_135030_wcf01.gif


呼叫中心运行在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(Mcrosoft Message Queuing)利用消息队列支持应用程序之间的消息传递。
如此看来,要建立一个好的汽车租赁预约服务系统,需要用到的.Net分布式技术,包括.Net Remoting、Web Service,COM+等五种技术,这既不利于开发者的开发,也加大了程序的维护难度和开发成本。正是因应于这样的缺陷,WCF才会在.Net 2.0中作为全新的分布式开发技术被微软强势推出,它整合了上述所属的分布式技术,成为了理想的分布式开发的解决之道。下图展示了WCF与之前的相关技术的比较:

uploads/200708/06_135034_wcf02.gif


从功能的角度来看,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间的通信,如下图所示:

uploads/200708/06_135039_wcf03.gif


可以跨进程、跨机器甚至于跨平台的通信,只要支持标准的Web Service,例如J2EE应用服务器(如WebSphere,WebLogic)。应用程序可以运行在Windows操作系统下,也可以运行在其他的操作系统,如Sun Solaris,HP Unix,Linux等等。如下图所示:

uploads/200708/06_135043_wcf04.gif


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)。如图所示:

uploads/200708/06_135047_wcf05.gif


4、兼容性
WCF充分的考虑到了与旧有系统的兼容性。安装WCF并不会影响原有的技术如ASMX和.Net Remoting。即使对于WCF和ASMX而言,虽然两者都使用了SOAP,但基于WCF开发的应用程序,仍然可以直接与ASMX进行交互。

示例下载(VS2005 下编写)


三、WCF的技术要素



作为基于SOA(Service Oriented Architecture)的一个框架产品,WCF最重要的就是能够快捷的创建一个服务(Service)。如下图所示,一个WCF Service由下面三部分构成:

uploads/200708/06_141701_wcf06.gif


1、Service Class:一个标记了[ServiceContract]Attribute的类,在其中可能包含多个方法。除了标记了一些WCF特有的Attribute外,这个类与一般的类没有什么区别。
2、Host(宿主):可以是应用程序,进程如Windows Service等,它是WCF Service运行的环境。
3、Endpoints:可以是一个,也可以是一组,它是WCF实现通信的核心要素。

WCF Service由一个Endpoints集合组成,每个Endpoint就是用于通信的入口,客户端和服务端通过Endpoint交换信息,如下图所示:

uploads/200708/06_141710_wcf07.gif


从图中我们可以看到一个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,如下图:

uploads/200708/06_141715_wcf08.gif


EndpointAddress类又包含URI,Identity和可选的headers集合组成,如下图:

uploads/200708/06_141721_wcf09.gif


Endpoint安全的唯一性识别通常是通过其URI的值,但为了避免一些特殊情况造成URI的重复,又引入了Identity附加到URI上,保证了Endpoint地址的唯一性。至于可选的AddressHeader则提供了一些附加的信息,尤其是当多个Endpoint在使用同样的URI地址信息时,AddressHeader就非常必要了。

Binding类(位于System.ServiceModel.Channels命名空间)包含Name,Namespace和BindingElement集合,如下图:

uploads/200708/06_141727_wcf10.gif


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的组成如下图所示:

uploads/200708/06_141732_wcf11.gif


正如在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类的组成如下图所示:

uploads/200708/06_141738_wcf12.gif


我们可以利用代码的方式创建ServiceDescription对象,也可以利用WCF的Attribute,或者使用工具SvcUtil.exe。虽然可以显式的创建它,但通常情况下,它会作为运行中的Service一部分而被隐藏于后(我在后面会提到)。

ChannelDescription类的组成与ServiceDescription大致相同,但它仅仅包含了一个ServiceEndpoint,用于表示客户端通过通道通信的目标Endpoint。当然,施加到ChannelDescription的Behavior也相应的为IChannelBehavior接口类型,如图所示:

uploads/200708/06_141743_wcf13.gif


定义一个WCF Service非常简单,以SayHello为例,定义的Service可能如下:
  1. using System.ServiceModel   
  2.   
  3.     [ServiceContract]   
  4.     public class Service1   
  5.     {   
  6.         public string SayHello(string name)   
  7.         {   
  8.             return "Hello: " + name;   
  9.         }   
  10.     }  
System.ServiceModel是微软为WCF提供的一个新的类库,以用于面向服务的程序设计。在开发WCF应用程序时,需要先添加对System.ServiceModel的引用。WCF中的大部分类和接口也都是在命名空间System.ServiceModel下。

我们为Service1类标记了[ServiceContract],这就使得该类成为了一个WCF Service,而其中的方法SayHello()则因为标记了[OperationContract],而成为该Service的一个Operation。

不过WCF推荐的做法是将接口定义为一个Service,这使得WCF Service具有更好的灵活性,毕竟对于一个接口而言,可以在同时有多个类实现该接口,这也就意味着可以有多个Service Contract的实现。那么上面的例子就可以修改为:
  1. [ServiceContract()]   
  2. public interface IService1   
  3. {   
  4.     [OperationContract]   
  5.     string SayHello(string name);   
  6. }  
而类Service1则实现该IService1接口:
  1. public class Service1 : IService1   
  2. {   
  3.     public string SayHello(string name)   
  4.     {   
  5.         return "Hello: " + name;   
  6.     }   
  7. }  
注意在实现了IService1接口的类Service1中,不再需要在类和方法中标注ServiceContractAttribute和OperationContractAttribute了。

前面我已经提过,一个WCF Service必须有host作为它运行的环境。这个host可以是ASP.Net,可以是Windows Service,也可以是一个普通的应用程序,例如控制台程序。下面就是一个Host的实现:
  1. using System.ServiceModel   
  2.   
  3.     class HostApp   
  4.     {   
  5.         static void Main(string[] args)   
  6.         {   
  7.             MyServiceHost.StartService();   
  8.             Console.WriteLine("服务已经启动...");   
  9.             Console.Read();   
  10.             MyServiceHost.StopService();             
  11.         }   
  12.     }   
  13.   
  14.     internal class MyServiceHost   
  15.     {   
  16.         internal static ServiceHost myServiceHost = null;   
  17.   
  18.         internal static void StartService()   
  19.         {   
  20.             //Consider putting the baseAddress in the configuration system   
  21.             //and getting it here with AppSettings   
  22.             Uri baseAddress = new Uri("http:localhost:8080/service1");   
  23.   
  24.             //Instantiate new ServiceHost    
  25.             myServiceHost = new ServiceHost(typeof(WCFServiceLibrary2.Service1), baseAddress);   
  26.   
  27.             //Open myServiceHost   
  28.             myServiceHost.Open();   
  29.         }   
  30.   
  31.         internal static void StopService()   
  32.         {   
  33.             //Call StopService from your shutdown logic (i.e. dispose method)   
  34.             if (myServiceHost.State != CommunicationState.Closed)   
  35.                 myServiceHost.Close();   
  36.         }   
  37.     }  
在这个HostApp中,我们为Server1创建了一个ServiceHost对象。通过它就可以创建WCF运行时(Runtime),WCF Runtime是一组负责接收和发送消息的对象。ServiceHost可以创建SerivceDescription对象,利用SerivceDescription,SercieHost为每一个ServiceEndpoint创建一个EndpointListener。ServiceHost的组成如下图:

uploads/200708/06_141748_wcf14.gif


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代码,以完成之间的消息传递,如图所示:

uploads/200708/06_141754_wcf15.gif


SvcUtil.exe是由WinFx Runtime Component SDK所提供的,如果安装SDK正确,可以在其中找到该应用工具。生成客户端Proxy代码的方法很简单,首先需要运行服务端Service。然后再命令行模式下运行下面的命令:
svcutil.exe http://localhost:8080/service1?wsdl

这样会在当前目录下产生两个文件service1.cs和output.config。前者最主要的就是包含了一个实现了Service1接口的Proxy对象,这个代理对象名为Service1Client,代码生成的结果如下:
  1. [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel""3.0.0.0")]   
  2. [System.ServiceModel.ServiceContractAttribute(ConfigurationName="IService1")]   
  3. public interface IService1   
  4. {   
  5.        
  6.     [System.ServiceModel.OperationContractAttribute(Action="http:tempuri.org/IService1/SayHello", ReplyAction="http:tempuri.org/IService1/SayHelloResponse")]   
  7.     string SayHello(string name);   
  8. }   
  9.   
  10. [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel""3.0.0.0")]   
  11. public interface IService1Channel : IService1, System.ServiceModel.IClientChannel   
  12. {   
  13. }   
  14.   
  15. [System.Diagnostics.DebuggerStepThroughAttribute()]   
  16. [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel""3.0.0.0")]   
  17. public partial class Service1Client : System.ServiceModel.ClientBase<IService1>, IService1   
  18. {   
  19.        
  20.     public Service1Client()   
  21.     {   
  22.     }   
  23.        
  24.     public Service1Client(string endpointConfigurationName) :    
  25.             base(endpointConfigurationName)   
  26.     {   
  27.     }   
  28.        
  29.     public Service1Client(string endpointConfigurationName, string remoteAddress) :    
  30.             base(endpointConfigurationName, remoteAddress)   
  31.     {   
  32.     }   
  33.        
  34.     public Service1Client(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :    
  35.             base(endpointConfigurationName, remoteAddress)   
  36.     {   
  37.     }   
  38.        
  39.     public Service1Client(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :    
  40.             base(binding, remoteAddress)   
  41.     {   
  42.     }   
  43.        
  44.     public string SayHello(string name)   
  45.     {   
  46.         return base.Channel.SayHello(name);   
  47.     }   
  48. }  
至于后者,则是WCF Service的配置信息,主要包含的是Endpoint中Address,Binding以及Contract的配置(在后续文章我会详细介绍)。

现在客户端就可以直接使用Service1Client对象,来完成与服务端的通信了:
  1. class ClientApp   
  2. {   
  3.     static void Main(string[] args)   
  4.     {   
  5.         Service1Client client = new Service1Client();   
  6.   
  7.         // 使用 "client" 变量在服务上调用操作。   
  8.         Console.WriteLine("请输入你的名字: ");   
  9.         string name = Console.ReadLine();   
  10.         Console.WriteLine(client.SayHello(name));   
  11.   
  12.         // 始终关闭客户端。   
  13.         client.Close();   
  14.   
  15.         Console.ReadLine();   
  16.   
  17.     }   
  18. }  
除了可以使用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来得到的。所以客户端的代码可以修改如下:
  1. using System.ServiceModel;   
  2. using System.ServiceModel.Description;   
  3. using System.ServiceModel.Channel   
  4.   
  5.     class ClientApp   
  6.     {   
  7.         static void Main(string[] args)   
  8.         {   
  9.             ServiceEndpoint httpEndpoint = new ServiceEndpoint(ContractDescription.GetContract(typeof(IService1)), new WSHttpBinding(), new EndpointAddress("http:localhost:8080/service1"));   
  10.   
  11.             using (ChannelFactory<IService1> factory = new ChannelFactory<IService1>(httpEndpoint))   
  12.             {   
  13.                 //创建IHello服务的代理对象;      
  14.                 IService1 service = factory.CreateChannel();   
  15.   
  16.                 Console.WriteLine("请输入你的名字: ");   
  17.                 string name = Console.ReadLine();   
  18.                 Console.WriteLine(service.SayHello(name));   
  19.             }   
  20.             Console.ReadKey();   
  21.         }     
  22.     }  
对于上面的代码,我们有两点需要注意:
1、采用这种方式,前提条件是客户端能够访问IHello接口。这也印证了之前我所叙述的最好使用interface来定义Service的好处。此外,为了保证部署的方便,有关Service的interface最好单独编译为一个程序集,便于更好的部署到客户端。
2、客户端必须知道服务端binding的方式以及address。

对于服务端而言,我们也可以直接在浏览器中打开该Service,在地址栏中输入http://localhost:8080/service1,如下图:

uploads/200708/06_161535_wcf16.gif


点击链接:http://localhost:8080/service1?wsdl,我们可以直接看到Service1的WSDL。注意到在这里我并没有使用IIS,实际上WCF内建了对httpsys的集成,允许任何应用程序自动成为HTTP listener。

示例下载

示例下载(Orcas 下编写)

四、Service Contract编程模型



(二)中,我以“SayHello”为例讲解了如何定义一个Service。其核心就是为接口或类施加ServiceContractAttribute,为方法施加OperationContractAttribute。在Service的方法中,可以接受多个参数,也可以有返回类型,只要这些数据类型能够被序列化。这样一种方式通常被称为本地对象,远程过程调用(local-object, Remoting-Procedure-Call)方式,它非常利于开发人员快速地进行Service的开发。

在Service Contract编程模型中,还有一种方式是基于Message Contract的。服务的方法最多只能有一个参数,以及一个返回值,且它们的数据类型是通过Message Contract自定义的消息类型。在自定义消息中,可以为消息定义详细的Header和Body,使得对消息的交换更加灵活,也更利于对消息的控制。

一个有趣的话题是当我们定义一个Service时,如果一个private方法被施加了OperationContractAttribute,那么对于客户端而言,这个方法是可以被调用的。这似乎与private对于对象封装的意义有矛盾。但是这样的规定是有其现实意义的,因为对于一个服务而言,服务端和客户端的需求往往会不一致。在服务端,该服务对象即使允许被远程调用,但本地调用却可能会因情况而异。如下面的服务定义:
 
  1. [ServiceContract]   
  2. public class BookTicket   
  3. {   
  4.    [OperationContract]   
  5.    public bool Check(Ticket ticket)   
  6.    {   
  7.       bool flag;   
  8.       //logic to check whether the ticket is none;   
  9.       return flag;   
  10.    }   
  11.    [OperationContract]   
  12.    private bool Book(Ticket ticket)   
  13.    {   
  14.      //logic to book the ticket   
  15.    }   
  16. }  
在服务类BookTicket中,方法Check和Book都是服务方法,但后者被定义成为private方法。为什么呢?因为对于客户而言,首先会检查是否还有电影票,然而再预定该电影票。也就是说这两项功能都是面向客户的服务,会被远程调用。对于Check方法,除了远程客户会调用该方法之外,还有可能被查询电影票、预定电影票、出售电影票等业务逻辑所调用。而Book方法,则只针对远程客户,只可能被远程调用。为了保证该方法的安全,将其设置为private,使得本地对象不至于调用它。

因此在WCF中,一个方法是否应该被设置为服务方法,以及应该设置为public还是private,都需要根据具体的业务逻辑来判断。如果涉及到私有的服务方法较多,一种好的方法是利用设计模式的Façade模式,将这些方法组合起来。而这些方法的真实逻辑,可能会散放到各自的本地对象中,对于这些本地对象,也可以给与一定的访问限制,如下面的代码所示:
  1. internal class BusinessObjA   
  2. {   
  3.    internal void FooA(){}   
  4. }   
  5. internal class BusinessObjB   
  6. {   
  7.    internal void FooB(){}   
  8. }   
  9. internal class BusinessObjC   
  10. {   
  11.    internal void FooC(){}   
  12. }   
  13. [ServiceContract]   
  14. internal class Façade   
  15. {   
  16.    private BusinessObjA objA = new BusinessObjA();   
  17.    private BusinessObjB objB = new BusinessObjB();   
  18.    private BusinessObjC objC = new BusinessObjC();   
  19.    [OperationContract]   
  20.    private void SvcA()   
  21.    {   
  22.       objA.FooA();   
  23.    }   
  24.  [OperationContract]   
  25.    private void SvcB()   
  26.    {   
  27.       objB.FooB();   
  28.    }   
  29.    [OperationContract]   
  30.    private void SvcC()   
  31.    {   
  32.       objC.FooC();   
  33.    }   
  34. }  
方法FooA,FooB,FooC作为internal方法,拒绝被程序集外的本地对象调用,但SvcA,SvcB和SvcC方法,却可以被远程对象所调用。我们甚至可以将BusinessObjA,BusinessObjB等类定义为Façade类的嵌套类。采用这样的方法,有利于这些特殊的服务方法,被远程客户更方便的调用。

定义一个Service,最常见的还是显式地将接口定义为Service。这样的方式使得服务的定义更加灵活,这一点,我已在(二)中有过描述。当然,采用这种方式,就不存在前面所述的私有方法成为服务方法的形式了,因为在一个接口定义中,所有方法都是public的。

另外一个话题是有关“服务接口的继承”。一个被标记了[ServiceContract]的接口,在其继承链上,允许具有多个同样标记了[ServiceContract]的接口。对接口内定义的OperationContract方法,则是根据“聚合”的原则,如下的代码所示:
  1. [ServiceContract]   
  2. public interface IOne   
  3. {   
  4.    [OperationContract(IsOneWay=true)]   
  5.    void A();   
  6. }   
  7. [ServiceContract]   
  8. public interface ITwo   
  9. {   
  10.    [OperationContract]   
  11.    void B();   
  12. }   
  13. [ServiceContract]   
  14. public interface IOneTwo : IOne, ITwo   
  15. {   
  16.    [OperationContract]   
  17.    void C();   
  18. }  
在这个例子中,接口IOneTwo继承了接口IOne和ITwo。此时服务IOneTwo暴露的服务方法应该为方法A、B和C。

然而当我们采用Duplex消息交换模式(文章后面会详细介绍Duplex)时,对于服务接口的回调接口在接口继承上有一定的限制。WCF要求服务接口IB在继承另一个服务接口IA时,IB的回调接口IBCallBack必须同时继承IACallBack,否则会抛出InvalidContractException异常。正确的定义如下所示:
  1. [ServiceContract(CallbackContract = IACallback)]   
  2. interface IA {}   
  3. interface IACallback {}   
  4.   
  5. [ServiceContract(CallbackContract = IBCallback)]   
  6. interface IB : IA {}   
  7. interface IBCallback : IACallback {}  

五、消息交换模式(Message Exchange Patterns,MEPS)



在WCF中,服务端与客户端之间消息的交换共有三种模式:Request/Reply,One-Way,Duplex。

1、Request/Reply
这是默认的一种消息交换模式,客户端调用服务方法发出请求(Request),服务端收到请求后,进行相应的操作,然后返回一个结果值(Reply)。

如果没有其它特别的设置,一个方法如果标记了OperationContract,则该方法的消息交换模式就是采用的Request/Reply方式,即使它的返回值是void。当然,我们也可以将IsOneWay设置为false,这也是默认的设置。如下的代码所示:
  1. [ServiceContract]   
  2. public interface ICalculator   
  3. {   
  4.    [OperationContract]   
  5.    int Add(int a, int b);   
  6.   
  7.    [OperationContract]   
  8.    int Subtract(int a, int b);   
  9. }  
2、One-Way
如果消息交换模式为One-Way,则表明客户端与服务端之间只有请求,没有响应。即使响应信息被发出,该响应信息也会被忽略。这种方式类似于消息的通知或者广播。当一个服务方法被设置为One-Way时,如果该方法有返回值,会抛出InvalidOperationException异常。

要将服务方法设置为One-Way非常简单,只需要将OperationContractAttribute的属性IsOneWay设置为true就可以了,如下的代码所示:
  1. public class Radio   
  2. {   
  3.    [OperationContract(IsOneWay=true)]   
  4.    private void BroadCast();   
  5. }  
3、Duplex
Duplex消息交换模式具有客户端与服务端双向通信的功能,同时它的实现还可以使消息交换具有异步回调的作用。

要实现消息交换的Duplex,相对比较复杂。它需要定义两个接口,其中服务接口用于客户端向服务端发送消息,而回调接口则是从服务端返回消息给客户端,它是通过回调的方式来完成的。接口定义如下:
服务接口:
  1. [ServiceContract(SessionMode=SessionMode.Allowed, CallbackContract=typeof(ICalculatorDuplexCallback))]   
  2. public interface ICalculatorDuplex   
  3. {   
  4.   
  5.     [OperationContract(IsOneWay=true)]   
  6.     void Clear();   
  7.   
  8.     [OperationContract(IsOneWay=true)]   
  9.     void AddTo(double n);   
  10.   
  11.     [OperationContract(IsOneWay = true)]   
  12.     void SubtractFrom(double n);   
  13. }  
回调接口:
  1. public interface ICalculatorDuplexCallback   
  2. {   
  3.     [OperationContract(IsOneWay=true)]   
  4.     void Equals(double result);   
  5.   
  6.     [OperationContract(IsOneWay=true)]   
  7.     void Equation(string equation);   
  8. }  
注意在接口定义中,每个服务方法的消息转换模式均设置为One-Way。此外,回调接口是被本地调用,因此不需要定义[ServiceContract]。在服务接口中,需要设置ServiceContractAttribute的CallbackContract属性,使其指向回调接口的类型type。

对于实现服务的类,实例化模式(InstanceContextMode)究竟是采用PerSession方式,还是PerCall方式,应根据该服务对象是否需要保存状态来决定。如果是PerSession,则服务对象的生命周期是存活于一个会话期间。而PerCall方式下,服务对象是在方法被调用时创建,结束后即被销毁。然而在Duplex模式下,不能使用Single方式,否则会导致异常抛出。本例的实现如下:
  1. [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]   
  2. public class CalculatorService : ICalculatorDuplex   
  3. {   
  4.     double result;   
  5.     string equation;   
  6.     ICalculatorDuplexCallback callback;   
  7.   
  8.     public CalculatorService()   
  9.     {   
  10.         result = 0.0;   
  11.         equation = result.ToString();   
  12.         callback = OperationContext.Current.GetCallbackChannel<ICalculatorDuplexCallback>();   
  13.   
  14.     }  
  15.  
  16.     #region ICalculatorDuplex Members   
  17.   
  18.     public void Clear()   
  19.     {   
  20.         equation += "=" + result.ToString();   
  21.         callback.Equation(equation);   
  22.   
  23.     }   
  24.   
  25.     public void AddTo(double n)   
  26.     {   
  27.         result += n;   
  28.         equation += "+" + n.ToString();   
  29.         callback.Equals(result);   
  30.     }   
  31.   
  32.     public void SubtractFrom(double n)   
  33.     {   
  34.         result -= n;   
  35.         equation += "-" + n.ToString();   
  36.         callback.Equals(result);   
  37.     }  
  38.  
  39.     #endregion   
  40. }  
在类CalculatorService中,回调接口对象callback通过OperationContext.Current.GetCallbackChannel<>()获取。然后在服务方法例如AddTo()中,通过调用该回调对象的方法,完成服务端向客户端返回消息的功能。

在使用Duplex时,Contract使用的Binding应该是系统提供的WSDualHttpBinding,如果使用BasicHttpBinding,会出现错误。因此WCF Service Host程序配置文件应该如下所示:
  1. <?xml version="1.0" encoding="utf-8" ?>  
  2. <configuration>  
  3.   <!-- When deploying the service library project, the content of the config file must be added to the host's    
  4.   app.config file. System.Configuration does not support config files for libraries. -->  
  5.   <system.serviceModel>  
  6.     <services>  
  7.       <service name="WcfServiceLibrary1.CalculatorService" behaviorConfiguration="WcfServiceLibrary1.Service1Behavior">  
  8.         <host>  
  9.           <baseAddresses>  
  10.             <add baseAddress = "http:localhost:8080/Service1" />  
  11.           </baseAddresses>  
  12.         </host>  
  13.         <!-- Service Endpoints -->  
  14.         <!-- Unless fully qualified, address is relative to base address supplied above -->  
  15.         <endpoint address ="" binding="wsDualHttpBinding" contract="WcfServiceLibrary1.ICalculatorDuplex" />  
  16.       </service>  
  17.     </services>  
  18.     <behaviors>  
  19.       <serviceBehaviors>  
  20.         <behavior name="WcfServiceLibrary1.Service1Behavior">  
  21.           <!-- To avoid disclosing metadata information,    
  22.           set the value below to false and remove the metadata endpoint above before deployment -->  
  23.           <serviceMetadata httpGetEnabled="True"/>  
  24.           <!-- To receive exception details in faults for debugging purposes,    
  25.           set the value below to true.  Set to false before deployment    
  26.           to avoid disclosing exception information -->  
  27.           <serviceDebug includeExceptionDetailInFaults="False" />  
  28.         </behavior>  
  29.       </serviceBehaviors>  
  30.     </behaviors>  
  31.   </system.serviceModel>  
  32. </configuration>  
使用命令svcutil.exe http://localhost:8080/Service1?wsdl生成客户端代理类和配置文件,配置文件内容类似:
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <configuration>  
  3.     <system.serviceModel>  
  4.         <client>  
  5.             <endpoint address="http:localhost:8080/Service1"    
  6.                       binding="wsDualHttpBinding"  
  7.                       contract="ICalculatorDuplex">  
  8.                 <identity>  
  9.                     <userPrincipalName value="allisok-PC\allisok" />  
  10.                 </identity>  
  11.             </endpoint>  
  12.         </client>  
  13.     </system.serviceModel>  
  14. </configuration>  
当服务端将信息回送到客户端后,对消息的处理是由回调对象来处理的,所以回调对象的实现应该是在客户端完成,如下所示的代码应该是在客户端中:
  1. public class CallbackHandler : ICalculatorDuplexCallback   
  2. {  
  3.     #region ICalculatorDuplexCallback Members   
  4.   
  5.     void ICalculatorDuplexCallback.Equals(double result)   
  6.     {   
  7.         Console.WriteLine("Result({0})", result);   
  8.     }   
  9.   
  10.     void ICalculatorDuplexCallback.Equation(string equation)   
  11.     {   
  12.         Console.WriteLine("Equation({0})", equation);   
  13.     }  
  14.  
  15.     #endregion   
  16. }  
客户端调用服务对象相应的为:
  1. class ClientApp   
  2. {   
  3.     static void Main(string[] args)   
  4.     {   
  5.         InstanceContext instanceContext = new InstanceContext(new CallbackHandler());   
  6.   
  7.         CalculatorDuplexClient client = new CalculatorDuplexClient(instanceContext);   
  8.   
  9.         // 使用 "client" 变量在服务上调用操作。   
  10.         double value;   
  11.         value = 100;   
  12.         client.AddTo(value);   
  13.   
  14.         value = 50;   
  15.         client.SubtractFrom(value);   
  16.   
  17.         client.Clear();   
  18.            
  19.         // 始终关闭客户端。   
  20.         //client.Close();   
  21.   
  22.         Console.ReadLine();   
  23.   
  24.     }   
  25. }  
注意在Duplex中,会话创建的时机并不是客户端创建Proxy实例的时候,而是当服务对象的方法被第一次调用时,会话方才建立,此时服务对象会在方法调用之前被实例化,直至会话结束,服务对象都是存在的。


六、定义DataContract



我在介绍如何定义一个ServiceContract时,举了这样的一个例子,代码如下:
  1. [ServiceContract]   
  2. public class BookTicket   
  3. {   
  4.    [OperationContract]   
  5.    public bool Check(Ticket ticket)   
  6.    {   
  7.       bool flag;   
  8.       //logic to check whether the ticket is none;   
  9.       return flag;   
  10.    }   
  11.    [OperationContract]   
  12.    private bool Book(Ticket ticket)   
  13.    {   
  14.       //logic to book the ticket   
  15.    }   
  16. }  
在Service类BookTicket中,两个服务方法Check和Book的参数均为Ticket类型。这个类型是自定义类型,根据WCF的要求,该类型必须支持序列化的操作,方才可以在服务方法中作为消息被传递。

在.Net中,除了基本类型如int,long,double,以及枚举类型和String类型外,一个自定义的类型如要支持序列化操作,应该标记该类型为[Serializable],或者使该类型实现ISerializable接口。而在WCF中,推荐的一种方式是为这些类型标记DataContractAttribute。方法如下:
  1. [DataContract]   
  2. public class Ticket   
  3. {   
  4. private string m_movieName;   
  5.   
  6.    [DataMember]   
  7.    public int SeatNo;   
  8.    [DataMember]   
  9.    public string MovieName   
  10.    {   
  11.       get {return m_movieName;}   
  12.       set {m_movieName = value;}   
  13.    }   
  14.    [DataMember]   
  15.    private DateTime Time;   
  16. }  
其中,[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成员包含了泛型,那么泛型类型参数必须支持序列化,如下代码所示:
  1. [DataContract]   
  2. public class MyGeneric<T>   
  3. {   
  4.     [DataMember]   
  5.     public T theData;   
  6. }  
在类MyGeneric中,泛型参数T必须支持序列化。如实例化该对象:
  1. MyGeneric<int> intObject = new MyGeneric();   
  2. MyGeneric<Custom> customObject = new MyGeneric();  
对象intObject由于传入的泛型参数为int基本类型,因此可以被序列化;而对象customObject是否能被序列化,则要看传入的泛型参数CustomType类型是否支持序列化。

DataContract以Namespace和Name来唯一标识,我们可以在DataContractAttribute的Namespace属性、Name属性中进行设置。如未设置DataContract的Name属性,则默认的名字为定义的类型名。DataMember也可以通过设置Name属性,默认的名字为定义的成员名。如下代码所示:
  1. namespace MyCompany.OrderProc   
  2. {   
  3.     [DataContract]   
  4.     public class PurchaseOrder   
  5.     {   
  6.         // DataMember名字为默认的Amount;   
  7.         [DataMember]   
  8.         public double Amount;   
  9.           
  10.         // Name属性将重写默认的名字Ship_to,此时DataMember名为Address;   
  11.         [DataMember(Name = "Address")]   
  12.         public string Ship_to;   
  13.     }   
  14.     //Namespace为默认值:   
  15.     // http:schemas.datacontract.org/2004/07/MyCompany.OrderProc   
  16.     //此时其名为PurchaseOrder而非MyInvoice   
  17.     [DataContract(Name = "PurchaseOrder")]   
  18.     public class MyInvoice   
  19.     {   
  20.         // Code not shown.   
  21.     }   
  22.   
  23.     // 其名为Payment而非MyPayment   
  24.     // Namespace被设置为http:schemas.example.com/   
  25.     [DataContract(Name = "Payment",   
  26.         Namespace = "http:schemas.example.com/")]   
  27.     public class MyPayment   
  28.     {   
  29.         // Code not shown.   
  30.     }   
  31. }  
// 重写MyCorp.CRM下的所有DataContract的Namespace
  1. // 3.0 的语法?   
  2. [assembly:ContractNamespace(   
  3.       ClrNamespace = "MyCorp.CRM",   
  4.       Namespace= "http:schemas.example.com/crm")]   
  5. namespace MyCorp.CRM   
  6. {   
  7.     // 此时Namespace被设置为http:schemas.example.com/crm.   
  8.     // 名字仍然为默认值Customer   
  9.     [DataContract]   
  10.     public class Customer   
  11.     {   
  12.         // Code not shown.   
  13.     }   
  14. }  
由于DataContract将被序列化以及反序列化,因此类型中成员的顺序也相当重要,在DataMemberAttribute中,提供了Order属性,用以设置成员的顺序。在WCF中对成员的序列化顺序规定如下:
1、默认的顺序依照字母顺序;
2、如成员均通过Order属性指定了顺序,且顺序值相同,则以字母顺序;
3、未指定Order属性的成员顺序在指定了Order顺序之前;
4、如果DataContract处于继承体系中,则不管子类中指定的Order值如何,父类的成员顺序优先。

下面的代码很好的说明了DataMember的顺序:
  1. [DataContract]   
  2. public class BaseType   
  3. {   
  4.     [DataMember] public string zebra;   
  5. }   
  6.   
  7. [DataContract]   
  8. public class DerivedType : BaseType   
  9. {   
  10.     [DataMember(Order = 0)] public string bird;   
  11.     [DataMember(Order = 1)] public string parrot;   
  12.     [DataMember] public string dog;   
  13.     [DataMember(Order = 3)] public string antelope;   
  14.     [DataMember] public string cat;   
  15.     [DataMember(Order = 1)] public string albatross;   
  16. }  
序列化后的XML内容如下:
  1. <DerivedType>  
  2.     <zebra/>  
  3.     <cat/>  
  4.     <dog/>  
  5.     <bird/>  
  6.     <albatross/>  
  7.     <parrot/>  
  8.     <antelope/>  
  9. </DerivedType>  
因为成员zebra为父类成员,首先其顺序在最前面。cat和dog未指定Order,故在指定了Order的其它成员之前,两者又按照字母顺序排列。parrot和albatross均指定Order值为1,因此也按照字母顺序排列在Order值为0的bird之后。

判断两个DataContract是否相同,应该根据DataContract的Namespace和Name,以及DataMember的Name和Order来综合判断。例如下面代码所示的类Customer和Person其实是同一个DataContract:
  1. [DataContract]   
  2. public class Customer   
  3. {   
  4.     [DataMember]   
  5.     public string fullName;   
  6.   
  7.     [DataMember]   
  8.     public string telephoneNumber;   
  9. }   
  10.   
  11. [DataContract(Name=”Customer”)]   
  12. public class Person   
  13. {   
  14.     [DataMember(Name = "fullName")]   
  15.     private string nameOfPerson;   
  16.   
  17.     private string address;   
  18.   
  19.     [DataMember(Name= "telephoneNumber")]   
  20.     private string phoneNumber;   
  21. }  
再例如下面代码所示的类Coords1、Coords2、Coords3也是相同的DataContract,而类Coords4则因为顺序不同,因而与前面三个类是不同的:
  1. [DataContract(Name= "Coordinates")]   
  2. public class Coords1   
  3. {   
  4.     [DataMember] public int X;   
  5.     [DataMember] public int Y;   
  6. }   
  7.   
  8. [DataContract(Name= "Coordinates")]   
  9. public class Coords2   
  10. {   
  11.      [DataMember] public int Y;   
  12.      [DataMember] public int X;   
  13. }   
  14.   
  15. [DataContract(Name= "Coordinates")]   
  16. public class Coords3   
  17. {   
  18.      [DataMember(Order=2)] public int Y;   
  19.      [DataMember(Order=1)] public int X;   
  20. }   
  21. [DataContract(Name= "Coordinates")]   
  22. public class Coords4   
  23. {   
  24.      [DataMember(Order=1)] public int Y;   
  25.      [DataMember(Order=2)] public int X;   
  26. }  
当DataContract处于继承体系时,还需要注意的是对象的“多态”问题。如果在服务端与客户端之间要传递消息,经常会涉及到类型的动态绑定。根据规定,如果消息的类型是子类类型,那么发送消息一方就不能传递基类类型。相反,如果消息类型是父类类型,那么发送消息一方就可以是父类本身或者其子类。从这一点来看,WCF的规定是与面向对象思想并行不悖的。但是可能存在的问题是,当消息类型定义为父类类型,而发送消息一方传递其子类时,服务端有可能对该子类类型处于“未知”状态,从而不能正常地反序列化。所以,WCF为DataContract提供了KnownTypeAttribute,通过设置它来告知服务端可能存在的动态绑定类类型。

举例来说,如果我们定义了这样三个类:
  1. [DataContract]   
  2. public class Shape { }   
  3.   
  4. [DataContract(Name = "Circle")]   
  5. public class CircleType : Shape { }   
  6.   
  7. [DataContract(Name = "Triangle")]   
  8. public class TriangleType : Shape { }  
然后在类CompanyLogo中定义Shape类型的字段,如下所示:
  1. [DataContract]   
  2. public class CompanyLogo   
  3. {   
  4.     [DataMember]   
  5.     private Shape ShapeOfLogo;   
  6.     [DataMember]   
  7.     private int ColorOfLogo;   
  8. }  
此时的CompanyLogo类由于正确的设置了[DataContract]和[DataMember],而Shape类型也是支持序列化的,因此该类型是可以被序列化的。然而一旦客户端在调用CompanyLogo类型的对象时,为字段ShapeOfLogo设置其值为CircleType或TriangleType类型的对象,就会发生反序列化错误,因为服务端并不知道CircleType或TriangleType类型,从而无法进行正确的匹配。所以上述的CompanyLogo类的定义应修改如下:
  1. [DataContract]   
  2. [KnownType(typeof(CircleType))]   
  3. [KnownType(typeof(TriangleType))]   
  4. public class CompanyLogo   
  5. {   
  6.     [DataMember]   
  7.     private Shape ShapeOfLogo;   
  8.     [DataMember]   
  9.     private int ColorOfLogo;   
  10. }  
类的继承如此,接口的实现也是同样的道理,如下例所示:
  1. public interface ICustomerInfo   
  2. {   
  3.     string ReturnCustomerName();   
  4. }   
  5.   
  6. [DataContract(Name = "Customer")]   
  7. public class CustomerType : ICustomerInfo   
  8. {   
  9.     public string ReturnCustomerName()   
  10.     {   
  11.         return "no name";   
  12.     }   
  13. }   
  14.   
  15. [DataContract]   
  16. [KnownType(typeof(CustomerType))]   
  17. public class PurchaseOrder   
  18. {   
  19.     [DataMember]   
  20.     ICustomerInfo buyer;   
  21.   
  22.     [DataMember]   
  23.     int amount;   
  24. }  
由于PurchaseOrder中定义了ICustomerInfo接口类型的字段,如要该类能被正确的反序列化,就必须为类PurchaseOrder加上[KnownType(typeof(CustomerType))]的标注。

对于集合类型也有相似的规定。例如Hashtable类型,其内存储的均为object对象,但实际设置的值可能是一些自定义类型,此时也许要通过KnownType进行标注。例如在类LibraryCatalog中,定义了Hashtable类型的字段theCatalog。该字段可能会设置为Book类型和Magazine类型,假定Book类型和Magazine类型均被定义为DataContract,则类LibraryCatalog的正确定义应如下所示:
  1. [DataContract]   
  2. [KnownType(typeof(Book))]   
  3. [KnownType(typeof(Magazine))]   
  4. public class LibraryCatalog   
  5. {   
  6.     [DataMember]   
  7.     System.Collections.Hashtable theCatalog;   
  8. }  
如果在一个DataContract中,定义一个object类型的字段。由于object类型是所有类型的父类,所以需要我们利用KnownType标明客户端允许设置的类型。例如类MathOperationData:
  1. [DataContract]   
  2. [KnownType(typeof(int[]))]   
  3. public class MathOperationData   
  4. {   
  5.     private object numberValue;   
  6.     [DataMember]   
  7.     public object Numbers   
  8.     {   
  9.         get { return numberValue; }   
  10.         set { numberValue = value; }   
  11.     }   
  12.     //[DataMember]   
  13.     //public Operation Operation;   
  14. }  
属性Numbers其类型为object,而KnownType设置的类型是int[],因此可以接受的类型就包括:整型,整型数组以及List类型。如下的调用都是正确的:
  1. static void Run()   
  2. {   
  3.     MathOperationData md = new MathOperationData();   
  4.   
  5.     int a = 100;   
  6.     md.Numbers = a;   
  7.   
  8.     int[] b = new int[100];   
  9.     md.Numbers = b;   
  10.       
  11.     List c = new List();   
  12.     md.Numbers = c;      
  13. }  
但如果设置Number属性为ArrayList,即使该ArrayList对象中元素均为int对象,也是错误的:
  1. static void Run()   
  2. {   
  3.     MathOperationData md = new MathOperationData();   
  4.   
  5. ArrayList d = new ArrayList();   
  6.     md.Numbers = d;   
  7. }  
一旦一个DataContract类型标注了KnownTypeAttribute,则该Attribute的作用域可以施加到其子类中,如下所示:
  1. [DataContract]   
  2. [KnownType(typeof(CircleType))]   
  3. [KnownType(typeof(TriangleType))]   
  4. public class MyDrawing   
  5. {   
  6.     [DataMember]   
  7.     private object Shape;   
  8.     [DataMember]   
  9.     private int Color;   
  10. }   
  11.   
  12. [DataContract]   
  13. public class DoubleDrawing : MyDrawing   
  14. {   
  15.     [DataMember]   
  16.     private object additionalShape;   
  17. }  
虽然DoubleDrawing没有标注KnowTypeAttribute,但其字段additionalShape仍然可以被设置为CircleType类型或TriangleType类型,因为其父类已经被设置为KnowTypeAttribute。

注:KnowTypeAttribute可以标注类和结构,但不能标注接口。此外,DataContract同样不能标注接口,仅可以标注类、结构和枚举。要使用DataContractAttribute、DataMemberAttribute和KnownTypeAttribute,需要添加WinFx版本的System.Runtime.Serialization程序集的引用。

http://www.xwy2.com/article.asp?id=23
posted on 2008-06-12 22:10  莫问奴归处  阅读(810)  评论(0编辑  收藏  举报
轩轩娃