虽然已经在多个项目中成功应用过WCF,但是感觉自己对WCF的知识只知道一些皮毛而已。上次学习WCF也是为了项目需要,囫囵吞枣。这不是我学习方法的态度。所以时至今日,又重新拾卷,再仔细的将WCF一些细节知识,边边角角自己回顾一番。
Host
三种Host的方式:IIS Host、WAS Host、Self-Host。
IIS Host
IIS这种非常简单,但只支持HTTP协议。不过,你可以借助IIS来管理服务的生命周期。在IIS上发布WCF Service是极其简单的,只需要写一个后缀名为svc的文件就ok了:
<%@ ServiceHost Language=”C#” Debug=”false” CodeBehind=”~/App_Code/MyService.cs” Service=”MyService” %>
还记得Web Service的asmx文件么?是不是如出一辙!
Self-Host
顾名思义,就是自托管了。开发人员完全控制,你可以将服务驻留在Console Application、WinForm、Windows Service。只需要保证服务先于客户端启动就Ok了。使用这种托管方式,开发人员可以完全的控制,可以使用任何协议,灵活性最大。
WAS Host
WAS的全称是Windows Activation Service,是个系统服务,跟随Vista发布的,是IIS 7的一部分,但是也可以单独安装和配置。要使用WAS,和IIS Host一样,也需要提供一个svc文件。但是,WAS不仅仅可以使用HTTP,可以使用WCF可以使用的任何协议。
WAS提供了很多Self-Host没有的优点,比如应用程序池、回收、身份管理等。
Endpoint
WCF中最重要的概念莫过于Endpoint了,Endpoint就是服务的接口。一个Endpoint包括三个要素:Address、Binding、Contract。
这三个方面实际上表达的是Where?How?What?的意思。
Address就是我到哪里(Where)寻找这个服务?
Binding就是我如何(How)与这个服务交互?
Contract就是这个服务是什么(What)?
每个Endpoint必须具有三个要素,缺一不可。
ServiceHost
Host架构
Service就驻留在ServiceHost实例中,每个ServiceHost实例只能托管一个服务。每个ServiceHost可以有多个Endpoint。一个进程里面可以有多个ServiceHost实例,同个宿主进程里不同的ServiceHost实例可以享用相同的BaseAddress。
使用Visual Studio自动生成服务端
Visual Studio的项目模板里已经为我们准备了WCF Service Application,使用Visual Studio创建的WCF Service Application项目默认是使用IIS托管的(WAS的托管方式与IIS的类似):
如果采用Selft-Host该怎么办呢?Visual Studio里还有一个WCF Service Library的项目模板,我们可以使用这个模板为Self-Host生成很多代码:
添加后生成的工程(经修改):
但不管是WCF Service Application还是WCF Service Library,我觉得这种自动生成的方式都不太好。从上面几个图我们可以看出,这两种项目模板都将服务契约与服务的实现放在同一个项目中,最后编译出来服务契约与服务实现也在同一个程序集中,既然如此那为何又要将契约和服务分开?不是多次一举么?对于Best Practice来讲,我们应该永远都为每个服务创建一个接口,而将[ServiceContract]特性加在这些接口上,然后在另一个项目里编写服务的实现类,引用服务契约的项目,实现这些接口(契约)。所以无论从学习还是Best Practice来讲,我们都应该具有手动从头到尾编写服务契约、实现服务、服务托管的代码的能力:
代码示例:
1: //订单项
2: [DataContract]
3: public class OrderItem
4: {
5: [DataMember]
6: public int Id{get;set;}
7: [DataMember]
8: public int ProductId{get;set;}
9: }
10: //订单
11: [DataContract]
12: public class Order
13: {
14: [DataMember]
15: public int OrderId{get;set;}
16:
17: [DataMember]
18: public IList<OrderItem> OrderItems{get;set;}
19: }
1: //订单服务契约
2: [ServiceContract]
3: public interface IOrderService
4: {
5: [OperationContract]
6: bool CreateOrder(Order order);
7:
8: [OperationContract]
9: bool DeleteOrder(int orderId);
10:
11: [OperationContract]
12: bool CancelOrder(int orderId);
13: }
1: //订单服务
2: public class OrderService : IOrderService
3: {
4: public void CreateOrder(Order order)
5: {
6: return true;
7: }
8: public bool DeleteOrder(int orderId)
9: {
10: return false;
11: }
12: public bool CancelOrder(int orderId)
13: {
14: return false;
15: }
16: }
服务托管(Self-Host)
1: static void Main()
2: {
3: //binding,how?
4: Binding tcpBinding = new NetTcpBinding();
5: ServiceHost host = new ServiceHost(typeof(OrderService),new Uri("net.tcp://localhost:8000/"));
6: host.AddServiceEndpoint(typeof(IOrderService),tcpBinding,"OrderService");
7: host.Open();
8: Console.ReadLine();
9: host.Close();
10: }
使用配置的方式:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <services>
5: <!--注意,这里的name要与服务的类名是一致的-->
6: <service name="OrderService">
7: <host>
8: <baseAddresses>
9: <add baseAddress="net.tcp://localhost:8000/" />
10: </baseAddresses>
11: </host>
12: <endpoint contract="IOrderService" binding="netTcpBinding" address="OrderService" />
13: </service>
14: </system.serviceModel>
15: </configuration>
MetadataExchange(元数据交换)
启用元数据交换有居多的好处,客户端可以使用SvcUtil工具自动的从服务元数据中生成客户端代理已经数据契约。
WCF启用元数据交换有两种方式:
1、使用HttpGet
2、使用一个专门的Endpoint用来进行元数据交换
HttpGet
先来看实例,HttpGet:
1: Uri httpBaseAddress = new Uri("http://locahost/");
2: Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/");
3: ServiceHost host = new ServiceHost(typeof(OrderService),httpBaseAddress,tcpBaseAddress);
4: ServiceMetadataBehavior metadataBehavior;
5: metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
6: if(metadataBehavior == null)
7: {
8: metadataBehavior = new ServiceMetadataBehavior();
9: //启用http-get方式
10: metadataBehavior.HttpGetEnabled = true;
11: host.Description.Behaviors.Add(metadataBehavior);
12: }
13: host.AddServiceEndpoint(typeof(IOrderService),new NetTcpBinding(),"OrderService");
14: host.Open();
15: Console.ReadLine();
16: host.Close();
既然是Http-Get的方式,顾名思义,肯定是使用http协议,所以必须有一个http的baseaddress。上面是使用代码的方式启用http-get,看看如何用配置打开http-get:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <services>
5: <service name="OrderService" behaviorConfiguration="EnableHttpGetBehavior">
6: <host>
7: <baseAddresses>
8: <add baseAddress="net.tcp://localhost:8000/" />
9: <add baseAddress="http://localhost/" />
10: </baseAddresses>
11: </host>
12: <endpoint contract="IOrderService" binding="netTcpBinding" address="OrderService" />
13: </service>
14: <behaviors>
15: <serviceBehaviors>
16: <behavior name="EnableHttpGetBehavior">
17: <serviceMetadata httpGetEnabled="true" />
18: </behavior>
19: </serviceBehaviors>
20: </behaviors>
21: </system.serviceModel>
22: </configuration>
添加专用Endpoint
编程添加
1: Uri httpBaseAddress = new Uri("http://locahost/");
2: Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/");
3: ServiceHost host = new ServiceHost(typeof(OrderService),httpBaseAddress,tcpBaseAddress);
4: //虽然不使用http-get的方式,ServiceMetadataBehavior还是要加的,而且是先加这个Behavior,然后再添加
5: //专门用于元数据交换的Endpoint,否则会抛出异常
6: ServiceMetadataBehavior metadataBehavior;
7: metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
8: if(metadataBehavior == null)
9: {
10: metadataBehavior = new ServiceMetadataBehavior();
11: //使用这种方式,HttpGetEnabled是否为true都无所谓,HttpGetEnabled默认值是false
12: host.Description.Behaviors.Add(metadataBehavior);
13: }
14: host.AddServiceEndpoint(typeof(IOrderService),new NetTcpBinding(),"OrderService");
15: //注意这里
16: BindingElement bindingElement = new TcpTransportBindingElement();
17: Binding customBinding = new CustomBinding(bindingElement);
18: //添加一个专门用于元数据交换的Endpoint
19: host.AddServiceEndpoint(typeof(IMetadataExchange),customBinding,"MEX");
20: host.Open();
21: Console.ReadLine();
22: host.Close();
配置的方式添加
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <system.serviceModel>
4: <services>
5: <service name="OrderService" behaviorConfiguration="EnableHttpGetBehavior">
6: <host>
7: <baseAddresses>
8: <add baseAddress="net.tcp://localhost:8000/" />
9: <add baseAddress="http://localhost/" />
10: </baseAddresses>
11: </host>
12: <endpoint contract="IOrderService" binding="netTcpBinding" address="OrderService" />
13: <endpoint contract="IMetadataExchange" binding="mexHttpBinding" address="MEX" />
14: </service>
15: <behaviors>
16: <serviceBehaviors>
17: <behavior name="EnableHttpGetBehavior">
18: <serviceMetadata />
19: </behavior>
20: </serviceBehaviors>
21: </behaviors>
22: </system.serviceModel>
23: </configuration>
从上面的代码我们基本上就知道了如何启用元数据交换了。使用http-get的方式简单,只需要一条设置就ok了,但是只能使用http或https,使用专用的endpoint则可以使用所有的协议。很多内容写在注释里了,仔细阅读。
服务已经Host了,Endpoint也已添加了,现在就要看看如何编写Client端。
Client端编程
使用Visual Studio自动生成客户端
客户端是通过一个代理与服务端交互的,我们可以使用Visual Studio的Add Service Reference的功能添加远程服务,这样Visual Studio就会自动的帮我们生成客户端服务的代理。要使用Add Service Reference首先你得服务端必须启用了元数据交换。如果你是使用Visual Studio在同一个解决方案下创建的WCF Service Application或WCF Service Library,在Add Service Reference窗口中,还可以使用Discovery按钮自动的找到本解决方案下的所有服务:
在这个窗口中,点击“Advanced”还可以对生成的代理做一些设置,在Namespace里可以设置生成服务的命名空间。这样做非常简便、高效,而且,当服务端修改了什么,我们只需要在客户端项目的Service Reference文件件下选择对应的服务,然后点击右键中的“Update Service Reference”,客户端代理就可以自动更新成新版本了。具有这样的优势很有诱惑力,但我们对Visual Studio生成了什么还是一无所知,不能自己完全的控制。所以你可以决定自己编写客户端代理。
Client Proxy
1: public class OrderServiceProxy : ClientBase<IOrderService>,IOrderService
2: {
3: public OrderServiceProxy(){}
4: public OrderServiceProxy(string endpointName):base(endpointName){}
5: public OrderServiceProxy(Binding binding,EndpointAddress remoteAddress):base(binding,remoteAddress){}
6:
7: public bool DeleteOrder(int orderId)
8: {
9: return this.Channel.DeleteOrder(orderId);
10: }
11: public bool CancelOrder(int orderId)
12: {
13: return this.Channel.CancelOrder(orderId);
14: }
15: public bool CreateOrder(Order order)
16: {
17: return this.Channel.CreateOrder(order);
18: }
19: }
这样一个本地的代理就创建好了,如何去使用这个代理呢?也有两种方式,第一种,我们可以编写代码创建一个本地代理,第二种,我们可以将配置保存在配置文件中。
使用代理
编程方式
1: static void Main()
2: {
3: Binding tcpBinding = new NetTcpBinding();
4: EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000/OrderService");
5: OrderServiceProxy proxy = new OrderServiceProxy(tcpBinding,address);
6:
7: //打开到远程服务的连接
8: proxy.Open();
9: //调用远程服务,是不是像调用本地方法一样
10: proxy.CancelOrder(5);
11: //关闭
12: proxy.Close();
13:
14: }
编程的方式的优点是能得到编译器的检查,但是如果想修改一下,比如日后改为http协议访问就得修改源代码。我们还可以使用配置的方式,在上面代理的代码中,我们发现代理还有一个构造器接受一个“endpointName”的参数,这个参数就是指配置文件中Endpoint的名称:
1: <?xml version="1.0" encoding="utf-8" ?>
2: configuration>
3: <system.serviceModel>
4: <client>
5: <endpoint address="net.tcp://localhost:8000/OrderService"
6: binding="netTcpBinding"
7: contract="IOrderService" name="OrderService">
8: </endpoint>
9: </client>
10: </system.serviceModel>
11: </configuration>
然后可以这样使用代理:
1: OrderServiceProxy proxy = new OrderServiceProxy("OrderService");
2: proxy.Open();
3: proxy.CancelOrder(5);
4: proxy.Close();
这种方式虽然不能获得编译时的检查,配置文件如果写错了,只有等到运行时才可以发现,但是将配置保存在程序员可以带来非常大的灵活性。
使用ChannelFactory创建代理
实际上,还有一种方式:
1: Binding binding = new NetTcpBinding();
2: EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000/OrderService")
3: IOrderService proxy = ChannelFactory<IOrderService>.CreateChannel(binding,address );
4: //代理使用后一定要关闭,看看下面的方式
5: using(proxy as IDisposable)
6: {
7: proxy.Login();
8: }
9: //或者这种方式也可以
10: ICommunicationObject channel = proxy as ICommunicationObject;
11: channel.Close();
元数据除了协助Visual Studio发现服务,自动生成代码(客户端代理,数据契约)还有什么用?我们可以使用编程的方式访问元数据么?答案是肯定的,下一节我们看看如果使用编程方式访问元数据。
元数据导入
我们可以编写代码,判断一个服务是否提供我们期望的Contract,这将怎么实现呢?比如我们要做一个小程序,遍历出某个指定address里暴露的所有Contract。WCF为我们提供了MetadataExchangeClient类。
1: //MetadataExchangeClient的构造器有几个重载
2: MetadataExchangeClient mexClient = new MetadataExchangeClient(new Uri("net.tcp://localhost:8000/MEX"),
3: MetadataExchangeClientMode.MetadataExchange);
4: //GetMetadata方法也有好几个重载
5: MetadataSet metadataSet = mexClient.GetMetadata();
6: WsdlImporter importer = new WsdlImporter(metadataSet);
7: ServiceEndpointCollection endpoints = importer.ImportAllEndpoints();
8: foreach(ServiceEndpoint endpoint in endpoints)
9: {
10: ContractDescription contract = endpoint.Contract;
11: Console.WriteLine("Namespace:{0},Name:{1}",contract.Namespace,contract.Name);
12: }
WCF架构
这个架构图对于日后的WCF扩展非常重要。
ServiceContract
契约与实现相分离
在项目中,一般都是有高程或架构师定义好服务契约,然后由程序员来实现这些服务契约。服务契约和实现最好是放在不同的项目中。这样的分离有什么好处呢?第一个,有的时候我们甚至想将服务的实现部分作为一个独立的部分去部署,比如在我曾经的一个项目中,用户要求软件脱机也能使用,在这个项目中WCF服务主要就是提供数据访问,我们的解决办法就是在客户端也部署一个实现服务的程序集,但这里并不使用WCF:
当然,你将服务契约和实现放在一起也可以这样部署,一点问题都没有,但在实现类上标有很多[ServiceContract]和[OperationContract],总有一点感觉这个东西职责不明一样(纯粹个人感觉而已)。
第二个优势是,就可以有很好的分工了。对于服务契约我觉得不是Team里的每个人都可以添加或修改的,只允许特定的人,比如架构师来做这件事情,作为程序员你只需要实现给定的契约就可以了,还有就是我在interface中定义的方法实现契约的人是必须实现的,不然编译都编译不过去,这也是一个强制性措施。
第三个优势是,一般客户端和服务器端都共享契约,那么我们可以在客户端和服务器端都引用这个契约的项目,减少代码量。
有了这些好处,我们就在interface中加[ServiceContract],在具体的类中实现这些interface。
操作契约的重载
使用C#这些面向对象的语言实现服务的时候,我们就得面临这样一个问题:重载。C#是允许重载的,但是重载属于面向对象的范畴,而WCF是SOA,在这里没有重载。在这里OperationContract为我们提供了一个Name属性,我们可以使用同名的方法,但是使用这里的Name区分开来。比如我们要编写一个用户验证的服务,我们可以使用username/password登陆系统或使用userid(会员编号)/password登陆系统:
1: [ServiceContract]
2: public interface IUserService
3: {
4: //使用重载,但是使用Name加以区分
5:
6: //使用username/password登陆
7: [OperationContract(Name="LoginUName")]
8: bool Login(string userName,string password);
9: //使用userid/password登陆
10: [OperationContract(Name="LoginUId")]
11: bool Login(int userId,string password);
12: }
如果你使用Visual Studio添加服务引用或SvcUtil生成客户端代理的代码,你会发现生成的代码是这样的:
1: [ServiceContract]
2: public interface IUserService
3: {
4: [OperationContract]
5: bool LoginUName(string userName,string password);
6:
7: [OperationContract]
8: bool LoginUId(int userId,string password);
9: }
因为元数据中携带的这两个方法的名字是Name指定的名字。这样就造成客户端和服务器端的不一致了,当然,你可以手动的修改这些方法名,然后在OperationContract的Name属性中指定与服务端一样的Name。不过我通常的做法是服务端和客户端引用相同的数据契约,我不知道这种做法是否合理,但却是减少了很多编码量。
有重载,就要谈到面向对象中的继承
继承
继承也是面向对象领域的东西,有的时候我们通过组合继承的层次,能减轻大量的重复工作。比如一个系统里有不同类型的会员,这些会员有一些共同的操作,比如登陆,修改密码等,但也有一些自己独有的操作,如果我们把共有的操作提取为一个服务契约,然后针对独立的再分开,这样就达到了契约的复用(注意,ServiceContract特性是不能继承的,所以在契约继承层次中每一个层都需要添加这个特性):
1: [ServiceContract]
2: public interface IUserService
3: {
4: [OperationContract]
5: bool Login(string userName,string password);
6: //other operations
7: }
8: [ServiceContract]
9: public interface IGoldUserService : IUserService
10: {
11: //黄金会员,可以购买更便宜的商品(只是举例说明继承,不考虑这种设计是否合理)
12: [OperationContract]
13: bool BuyCheaperProduct(int productId)
14: }
15: [ServiceContract]
16: public interface ISilverUserService : IUserService
17: {
18: //白银级会员,升级到黄金会员
19: [OperationContract]
20: bool UpdatetoGoldUser(int userId);
21: }
但是,我们如果通过元数据导入契约的时候却发现生成的契约不再有这种层次结构了,而是放在一个契约里,这个契约里包括直系层次中所有契约的操作,比如导入ISilverUserService的时候,却变成下面的样子:
1: [ServiceContract]
2: public interface ISilverUserService : IUserService
3: {
4: [OperationContract(Action=".../IUserService/Login"),ReplyAction=".../IUserService/LoginResponse"]
5: bool Login(string userName,string password);
6:
7: [OperationContract(Action=".../ISilverUserService/UpdatetoGoldUser",ReplyAction=".../ISilverUserService/UpdatetoGoldUserResponse")]
8: bool UpdatetoGoldUser(int userId);
9: }
注意这时在OperationContract特性中使用的Action和ReplyAction参数。当然,你也可以手动的重组这种层次关系,删除Action和ReplyAction。不过我常用的做法是,客户端和服务器端共享契约,共同引用契约所在的程序集,这是因为我们同一个team既开发服务端也开发客户端,但是如果你要导入远程第三方的服务就只能手动修改了。
这样一来,一个接收IUserService作为参数的方法也可以接收ISilverUserService作为参数,但是要记住,这里并不是IS-A的关系,因为这里是SOA不是OO。
服务的粒度
服务的粒度是个争论已久的话题,到底一个服务契约里面多好个操作才合适,就像在OO里一个类里多少个方法才合适。跟OO一样,在这里,你也最好把逻辑相关的操作放在同一个服务契约里面,什么是逻辑相关?就好比OO里同一职责吧。
由于一个ServiceHost实例只能承载一个服务实现,那么如果服务的粒度太小,势必造成系统中有很多小粒度的服务,就需要对应数量的ServiceHost实例,这是一件很烦的事情。但如果把很多逻辑不相关的操作都塞到一个服务里面也貌似不是很好。《Programming WCF Service》这本书上讲,每个服务6、7个操作最好,12个操作是上限(汗一个,上个项目中,一个服务我塞了60多个操作)。
DataContract
有操作那也要有数据吧,对于int、double这类简单的类型,都有标准系列化的方式,而对于Developer自己定义的类型如何去系列化和反系列化?这个就是数据契约要关心的内容了(DataContract)。
DataContract vs Serializable
在WCF之前我们有Web Service和Remoting,它们也需要系列化和反系列化。这种系列化方式就需要在被系列化的类型上使用[Serializbale]特性,而WCF中推出了一个新增的特性[DataContract],那么这两者有什么区别呢?
Serializable
标记有Serializable特性的类型里所有的字段或属性都会自动的被系列化,如果你不想系列化某个成员,可以在该成员上添加[Nonserializable]特性。
使用Serializable无法自定义类型和成员的名字
在传统系列化中,我们有两个系列化类:SoapFormatter和BinaryFormatter,这两个类都实现了IFormatter接口。
DataContract
只在类型上使用了[DataContract]还不行,还必须在每个想系列化的成员上添加[DataMember]特性
DataContract可以指定Namespace和Name属性,用来修改类型默认的名称,成员还有Name属性,用来修改成员默认的名称,有IsRequired属性和Order属性,这个会在后面提及。
在WCF中,新增了DataContractSerializer类,这个类继承自XmlObjectSerializer类,不支持IFormatter接口。WCF中还有一个NetDataContractSerializer类,这个类从XmlObjectSerializer派生,还实现了IFormatter接口。这个是为了兼容老的系列化模式。
也就是说DataContract的灵活性更高。在这里要注意的是,如果一个属性上使用了DataMember修饰了,这个属性的get和set存取器就都必须具备。因为系列化的时候要调用get存取器取值,而反系列化的时候要使用set存取器设置属性值。有一次一个同事访问WCF服务出现了异常,看了半天这个异常信息也没有查明白是咋回事儿,然后我们就采取最笨的方法:只保留DataContract里一个DataMember,逐步添加,最后终于找到这个元凶,原来他在一个不需要加DataMember的属性(该属性只有get访问器)加上了DataMember。
DataContract与Serializable是兼容的
虽然WCF中提供了新的DataContract特性,但你还是可以使用Serializable特性修饰。
XmlSerializer
唔,其实这个类,在我写这篇文章之前,我还不熟悉,为了这个类,我还输了一餐饭。我和一朋友打赌称在WCF之前,一个类要系列化必须在这个类上加[Serializable]特性,而Serializable特性又不能给类以及类的成员定义别名,最后没想到还有这么一个类。
这个XmlSerializer类在System.Xml命名空间下,这个类可以系列化那些即使没有添加[Serializable]特性的类型。而且使用这个类,还可以使用XmlType和XmlElement来指定系列化后的名称:
1: [XmlType(Namespace="Yuyijq",Name="U")]
2: public class User
3: {
4: [XmlElement("UName")]
5: public string UserName{get;set;}
6: public string Password{get;set;}
7: }
如果你要想你的WCF中某个方法使用XmlSerializer系列化参数,你可以在该该操作契约上添加[XmlSerializerFormat]特性,注意这个是添加到操作契约上的,也就是只影响这个操作,不会影响其他的,如果你要所有的操作都是用这种系列化方式,可以在使用SvcUtil工具生成客户端代理的时候使用/serializer:XmlSerializer指令。
DataContractJsonSerializer
DataContractJsonSerializer类是在.Net 3.5中新增的。看到这个名字你就应该知道这个类是干吗的了。嗯,就是将对象系列化为Json格式的字符串。啊?什么是Json?Json是JavaScript Object Notation的简称。如果不明白到底是个什么东西,请google之吧。如果你想在web上使用JavaScript调用服务(比如Ajax或Silverlight),那用这个系列化是最好的选择了。这个系列化器操作接口与其他的是一致的,但是如何让我们的WCF应用使用这种方式系列化呢?WCF在使用了WebScriptEnablingBehavior活配置了WebHttpBehavior时,会使用DataContractJsonSerializer系列化DataContract。
使用DataContractJsonSerializer,那么Operation接受的参数和返回的值,都将以Json的格式传递了,那么我们就可以在web页面直接用JavaScript调用WCF了。
系列化事件
在.Net 2.0中为系列化提供了四个事件,允许你将自己的代码注入到系列化的过程中。在WCF中也提供了这种机制:
1: [DataContract]
2: public class User
3: {
4: //other properties
5:
6: [OnSerializing]
7: void OnSerializing(StreamingContext context)
8: {
9: //在系列化之前
10: }
11: [OnSerialized]
12: void OnSerialized(StreamingContext context)
13: {
14: //系列化之后
15: }
16: [OnDeserializing]
17: void OnDeserializing(StreamingContext context)
18: {
19: //反系列化之前
20: }
21: [OnDeserialized]
22: void OnDeserialized(StreamingContext context)
23: {
24: //反系列化之后
25: }
26: }
注意,这些方法的签名是必须遵循的,方法名字可以自定义。而且同一种方法在一个类型里只能有一个,比如添加了OnSerializing特性的方法在这个User类里只能出现一次。
Known Type
与ServiceContract一样,DataContract也可以具有继承层次,但同样的道理,SOA里没有继承的概念,所以这里有些微妙:
1: [DataContract]
2: public class User
3: {
4: [DataMember]
5: public string UserName{get;set;}
6:
7: [DataMember]
8: public string Password{get;set;}
9: }
10:
11: [DataContract]
12: public class GoldUser
13: {
14: //折扣
15: [DataMember]
16: public double Discout{get;set;}
17: }
因为DataContract也是不支持继承的,所以在继承层次中每个类型上都要加。当有一个OperationContract接受GoldUser作为参数时,我们导入元数据生成客户端时,会自动生成继承层次中所有的类型的成员。继承的好处之一是什么?我们可以将子类当作父类使用,如果一个OperationContract期望得到User类型,但我们传入一个GoldUser,这在OO里面是可以的,但是在WCF里,编译通过,运行却会发生异常。因为这个服务根本不知道居然还有GoldUser的存在。那怎么办?WCF里有一个KnownType的特性,我们可以在父类上加上这个特性:
1: [DataContract]
2: [KnownType(typeof(GoldUser))]
3: public class User
4: {
5: [DataMember]
6: public string UserName{get;set;}
7:
8: [DataMember]
9: public string Password{get;set;}
10: }
11:
12: [DataContract]
13: public class GoldUser
14: {
15: //折扣
16: [DataMember]
17: public double Discout{get;set;}
18: }
现在,你就可以像OO里那样使用了。但是在DataContract里使用这个KnownType影响面太广了,可能有好多ServiceContract或OperationContract都是用了这个DataContract,但有的ServiceContract并不期望要利用这样的特性,它本来就只期望一个父类就OK了,所以WCF为我们准备了ServiceKnownType特性,这个特性既可以加在ServiceContract上,也可以加在OperationContract上:
1: [ServiceContract]
2: [ServiceKnownType(typeof(GoldUser))]
3: public interface IUserService
4: {
5: [OperationContract]
6: bool ChnagePassword(User user);
7: }
注意,KnownType特性和ServiceKnownType特性都可以多次添加,也就是说一个父类可以有多个KnowType。
虽然使用上面的特性貌似是解决了问题,但这太“荒谬”了。我们为什么要将一个子类作为父类参数传入?不仅仅是复用,还有一点就是松耦合。我可以添加更多的子类,还是可以往你传,但是你看现在,我添加一个子类我还必须在父类或服务上使用这个KnownType,那父类不就知道了每个子类的存在,这样还有意义么。实际上我们可以通过配置的方式配置KnownType,来达到代码上的解耦:
1: <system.runtime.serialization>
2: <dataContractSerializer>
3: <declaredTypes>
4: <add type="User,Domain,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null">
5: <KnownType type="GoldUser,Domain,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null" />
6: </add>
7: </declaredTypes>
8: <dataContractSerializer>
9: <system.runtime.serialization>
注意,上面配置文件中指定type的元素,必须用完全限定名,也就是name、version、culture、publickeytoken都必须提供,即使这是一个弱命名程序集(不知道为什么搞的这么麻烦)。
interface in DataContract
在DataContract中,除了可以类继承外,还可以实现接口的:
1: public interface IUser
2: {
3: string UserName{get;set;}
4: string Password{get;set;}
5: }
6: [DataContract]
7: public class User : IUser
8: {
9: [DataMember]
10: public string UserName{get;set;}
11: [DataMember]
12: public string Password{get;set;}
13: }
14:
15: [ServiceContract]
16: [ServiceTypeKnow(typeof(User))]
17: public interface IUserService
18: {
19: bool ChangePassword(IUser user);
20: }
然后,导入契约,my god,数据契约上的接口没有了,服务契约也变了:
1: [ServiceContract]
2: public interface IUserService
3: {
4: [OperationContract]
5: [ServiceTypeKnow(typeof(User))]
6: [ServiceTypeKnow(typeof(object))]
7: bool ChangePassword(object user);
8: }
当然,你可以通过手动的调整,调整成与服务端一致(不过我的做法是客户端服务端共享契约)。