【应用篇】WCF学习笔记(二):ServiceContract、DataContract <第一部分>

ServiceContract

契约与实现相分离

在项目中,一般都是有高程或架构师定义好服务契约,然后由程序员来实现这些服务契约。服务契约和实现最好是放在不同的项目中。这样的分离有什么好处呢?第一个,有的时候我们甚至想将服务的实现部分作为一个独立的部分去部署,比如在我曾经的一个项目中,用户要求软件脱机也能使用,在这个项目中WCF服务主要就是提供数据访问,我们的解决办法就是在客户端也部署一个实现服务的程序集,但这里并不使用WCF:
 offline

当然,你将服务契约和实现放在一起也可以这样部署,一点问题都没有,但在实现类上标有很多[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: }

当然,你可以通过手动的调整,调整成与服务端一致(不过我的做法是客户端服务端共享契约)。

原文地址:https://www.cnblogs.com/yuyijq/p/1590618.html