首页 > 代码库 > 跟我一起学WCF(7)——WCF数据契约与序列化详解

跟我一起学WCF(7)——WCF数据契约与序列化详解

一、引言

   在前面博文介绍到,WCF的契约包括操作契约、数据契约、消息契约和错误契约,前面一篇博文已经结束了操作契约的介绍,接下来自然就是介绍数据契约了。所以本文要分享的内容就是数据契约。

二、数据契约的介绍

   在WCF中,服务契约定义了可供调用的服务操作方法,而数据契约则是定义了服务端和客户端之间传送的自定义类型,在WCF项目中,必不可少地是传递数据,把客户端需要传递的数据传送到服务中,服务接收到数据再对其进行处理。然而在WCF中,传递的类型必须标记为DataContractAttribute属性,且只有标记了DataMemberAttribute属性的属性才会被传送。下面代码是一个数据契约使用的示例:

 1 [DataContract] // 数据契约属性声明 2     public class User 3     { 4         [DataMember(Name = "UserName")]//定义别名 5         public string Name 6         { get; set; } 7         [DataMember] 8         public string Password { get; set; } 9         [DataMember]10         public string Email { get; set; }11 12         // 没有[DataMember]声明将不会序列化传送13         public string Mobile { get; set; } 14 15         public string Test { get; set; }16     }

  上面代码在类User上使用了DataContract属性声明,则表明User类是可被WCF序列化程序可识别,并且可被序列化的。但是不是User所有数据成员都可以被需要列,只有声明了DataMemberAttribute的属性才可以被序列化。因此,在上面代码中,不会传输Mobile和Test的任何信息。同时也可以为声明为DataMember的成员定义客户端可见的别名,如DataMember(Name= "UserName"),这样在生成客户端代码时,User类定义的就是UserName属性,而不是在服务中定义的Name属性了。

三、序列化的详细介绍

  WCF的实现原理沿用了.NET Remoting的实现机制,客户端在调用服务公开的服务方法,这个过程必然涉及到数据的传输过程,包括客户端传输相关需要处理的数据给服务或服务传输相关处理后的结果数据给客户端。在数据传输的过程中,自然就需要进行序列化的操作,通过序列化把.NET Object序列化成可保存或传输的形式,然后通过网络协议在网络上进行传递。对于序列化的实现是由序列化器(Serializer)来负责完成的,序列化的实现原理可以理解为通过反射机制分析程序集中对应的类型,然后把对应的类型映射为一个XML的结构。

  序列化在.NET Framework相关专题就有所介绍,所以它并不是一个新的概念,相关内容可以参考MSDN:序列化。然而.NET本身的序列化机制在WCF程序中并不适应,所以WCF又提出了新的序列化器。下面分别介绍下.NET 序列化机制和WCF中序列化机制。

3.1 .NET序列化机制

   在.NET Framework 3.0之前,提供了3中序列化器,序列化器理解为把可序列化的类型序列化成XML的类。这三种序列化器分别是BinaryFormatter、SoapFormatter和XmlSerializer类。下面分别介绍下这3种序列化器。

  • BinaryFormatter类:把.NET Object序列化成二进制格式。在这个过程,对象的公共字段和私有字段以及类名称(包括类的程序集名),将转换成成字节流。
  • SoapFormatter类:把.NET Object序列化成SOAP格式,SOAP是一种轻量、简单的,基于XML的协议。只序列化字段,包括公共字段和私有字段
  • XmlSerializer类:该类仅仅序列化公共字段和属性,且不保存类型的保真度。

  对于这三种序列化机制,BinaryFormatter二进制序列化的优点是:性能高,但是不能跨平台。而SoapFormatter,XmlSerializer的优点是:跨平台、互操作性好,并且可读性强,但是传输性能不及BinaryFormatter。

  在.NET原有的序列化机制中,BinaryFormatter和SoapFormatter除了要序列化对象的状态信息外,还会将程序集和版本信息持久化到流中,因为只有这样才能保证对象呗反序列为正确的对象类型副本,这就要求客户端必须拥有原有的.NET 程序集,不能满足跨平台的需求。所以WCF不得不定义自己的序列化机制来满足面向服务的需求。

3.2 WCF中序列化机制

   在WCF中,提供了专门用来序列化和反序列操作的类,该类就是DataContractSerializer类。一般而言,WCF会自动选择使用DataContractSerializer来对可序列话数据契约进行序列化,不需要开发者直接调用。WCF除了支持DataContractSerializer类来进行序列化外,还支持另外两种序列化器,这两种序列化器分别为:XMLSerializer(定义在System.XML.Serialization namespace)和NetDataContractSerializer (定义在System.XML.Serialization namespace)。XmlSerializer类不是WCF专用的类,Asp.net Web服务统一使用该类作为序列化器,但XmlSerializer类支持的类少于DataContractSerializer列支持的类型,但它允许对生成的XML进行更多的控制,并且支持更多的XML架构定义语言(XSD)标准。它不需要在可序列化类上有任何声明性的属性。

  默认情况下,WCF 使用 DataContractSerializer 类来序列化数据类型。 此序列化程序支持下列类型:

  • 基元类型(如:整数、字符串和字节数组)以及某些特殊类型(如 XmlElement 和 DateTime)。
  • 数据协定类型(用 DataContractAttribute 属性标记的类型)。
  • 用 SerializableAttribute 属性标记的类型,包括实现 ISerializable 接口的类型。
  • 实现 IXmlSerializable 接口的类型。
  • 许多常见集合类型,包括许多泛型集合类型。

  DataContractSerializer类与NetDataContractSerializer类类似,它们之间主要的区别在于:在使用NetDataContractSerializer进行序列化时,不需要指定序列化的类型,如:

NetDataContractSerializer serializer =    new NetDataContractSerializer();  // 不需要明确指定序列化的类型serializer.WriteObject(writer, p);// 而使用DataContractSerializer需要明确指定序列化的类型DataContractSerializer serializer =            new DataContractSerializer(typeof(Order)); // 需要明确指定序列化的类型        serializer.WriteObject(writer, p);

四、WCF数据契约使用例子

   介绍了那么多关于数据契约和序列化内容的介绍,下面看看数据契约具体使用的例子。

  要使用数据契约,自然第一步是定义数据契约,具体数据契约的定义如下所示:

namespace BusinessEntity{    [DataContract]// 数据契约属性声明    public class User    {        [DataMember(Name = "UserName")]//定义别名        public string Name        { get; set; }        [DataMember]        public string Password { get; set; }        [DataMember]        public string Email { get; set; }        // 没有[DataMember]声明将不会序列化传送        public string Mobile { get; set; }         public string Test { get; set; }    }}

  第二步:定义完数据契约后,接下来就要定义我们的服务契约和服务契约的实现了。具体的实现代码如下所示:

 // 服务契约    [ServiceContract]    //[ServiceKnownType(typeof(Order))] // 这是为了演示WCF已知类型    public interface IUserValidationService    {        [OperationContract]        bool AddNewUser(User user);        [OperationContract]        User GetUserByName(string name);                // 为了演示已知类型的操作方法        //[OperationContract]        //[ServiceKnownType(typeof(Order))]        //bool AddOrder(OrderBase order);    } // 服务契约的实现    public class UserValidationService : IUserValidationService    {        public bool AddNewUser(User user)        {            return true;        }        public User GetUserByName(string name)        {            User user = new User { Name = name, Password = "123", Email = "123456@qq.com", Mobile = "13912331245" };            return user;        }        // 演示已知类型的操作方法        //public bool AddOrder(OrderBase order)        //{        //    return true;        //}    }

  对应的配置文件代码为:

 <system.serviceModel>          <behaviors>      <serviceBehaviors>        <behavior name="UserServiceBehavior">          <!-- To avoid disclosing metadata information, set the values below to false before deployment -->          <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>          <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->          <serviceDebug includeExceptionDetailInFaults="false"/>                  </behavior>      </serviceBehaviors>    </behaviors>    <protocolMapping>        <add binding="basicHttpsBinding" scheme="https" />    </protocolMapping>        <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />      <services>          <service name="WCFServiceAndHost.UserValidationService" behaviorConfiguration="UserServiceBehavior">              <endpoint address="" binding="wsHttpBinding" contract="WCFServiceAndHost.IUserValidationService"></endpoint>          </service>      </services>  </system.serviceModel>

  第三步:定义完服务之后,接下来就需要实现我们的客户端来访问服务方法了。首先,通过添加服务引用的方式来生成服务客户端代理类,生成的代理类中,User的定义如下代码所示:

[System.Diagnostics.DebuggerStepThroughAttribute()]    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "4.0.0.0")]    [System.Runtime.Serialization.DataContractAttribute(Name="User", Namespace="http://schemas.datacontract.org/2004/07/BusinessEntity")]    [System.SerializableAttribute()]    public partial class User : object, System.Runtime.Serialization.IExtensibleDataObject, System.ComponentModel.INotifyPropertyChanged {....}

  从上面代码标红的部分可以看出,服务中定义的User只应用了DataContractAttribute属性,但生成的客户端User类中多了一个SerializableAtttribute。对于SerializableAttribute属性的作用与DataContract的作用是一样的,都是标记为该类支持序列化。因为在默认情况下,用户定义的类型并不支持序列化,只有应用了SerializableAttribute或DataContractAttribute属性的,.NET序列化器才能对该类型进行序列化。然而这两者又存在不同, Serializable要求它的所有程序都要支持序列化,如果发现不支持序列化的成员就会抛出异常,即Serializable会把类型的所有成员都进行序列化,如果想某个成员不序列化化,则必须显式标记NoSerialized属性;而DataContract却不同,标记了DataContract属性的类只有标记了DataMember的成员才会被序列化,如果想类型的成员能够序列化,则应该应用DataMember属性。如果某个类型同时应用了DataContract和Serialized属性,如上面代码的User类,此时该类型将会只应用DataContract,即Serialized属性会忽略。我刚开始的疑问是,User类应用这两个属性,因为这两个属性对序列化成员有所区别,当时就纳闷到底是采取那个属性进行序列化的呢?经过查阅资料才发现了上面的结论,更多信息参考:Serialization in Windows Communication Foundation。

  然后利用该代理类实现对服务操作的调用,具体的实现代码如下所示:

namespace Client{    class Program    {        static void Main(string[] args)        {            UserValidationServiceClient wcfServiceProxy = new UserValidationServiceClient();            User newUser = new User() { UserName = "LearningHard", Email = "123456@qq.com", Password = "123" };            wcfServiceProxy.AddNewUser(newUser);            // 演示已知类型的问题            //Order order = new Order() { ID = Guid.NewGuid(), Date = DateTime.Now, Customer = "customer1", ShipAddress = "Shanghai", TotalPrice = 20.00 };            //wcfServiceProxy.AddOrder(order);            // 获得用户信息            string name = "Learning Hard Client";            User user = wcfServiceProxy.GetUserByName(name);            if (user != null)            {                Console.WriteLine("User Name is: " + user.UserName);                Console.WriteLine("Email is: " + user.Email);            }            Console.WriteLine("Press any key to continue...");            Console.Read();        }    }}

  经过上面的三步之后,我们就完成了WCF数据契约的实现。对于服务契约的调用过程是:客户端把相关需要序列化的对象序列化成XML格式,这里的格式与绑定的协议有关,因为上面设置的传输协议为http,所以这里应该序列化成XML格式的数据,然后再通过Http协议进行网络传递到服务,服务程序接收到传输过来的XML格式的数据,则利用DataContractSerializer反序列成User对象作为参数传递给AddNewUser方法;接着服务再把处理的后结果序列化成XML格式数据传递到客户端,客户端接收到服务程序响应的消息再进行反序列成具体的对象类型。对于操作GetUserByName的调用也是类似的。具体的运行结果如下图所示:

 

五、已知类型(KnownType)

   因为WCF中使用DataContractSerializer进行序列化和反序列化的,由于DataContractSerializer进行序列化和反序列化时,都必须事先确定对象的类型。如果被序列化对象或反序列化生成的对象包含不可知的类型,序列化或反序列化将失败。所以为了保证DataContractSerializer正常的序列化和反序列化,需要将“未知”类型加入DataContractSerializer“已知”类型列表中。例如下面的服务契约:

// 服务契约    [ServiceContract]    //[ServiceKnownType(typeof(Order))] // 这是为了演示WCF已知类型    public interface IUserValidationService    {        // 为了演示已知类型的操作方法        [OperationContract]        [ServiceKnownType(typeof(Order))]        bool AddOrder(OrderBase order);    }

  假如,客户端同时定义了一个Order类:

[DataContract]    public class Order : OrderBase    {        [DataMember]        public double TotalPrice { get; set; }    }

  以下代码能够成功通过编译,但在运行时却会失败:

 UserValidationServiceClient wcfServiceProxy = new UserValidationServiceClient();            // 演示已知类型的问题            Order order = new Order() { ID = Guid.NewGuid(), Date = DateTime.Now, Customer = "customer1", ShipAddress = "Shanghai", TotalPrice = 20.00 };            wcfServiceProxy.AddOrder(order);

  原因在于我们并没有实际传递对象的引用,而是传递的是对象的XML结构。在上面的例子中,当我们传递的是Order对象而不是OrderBase对象时,服务并不知道它应该反序列为Order对象。

  对于上面问题的解决办法就是让DataContractSerializer能够识别Order类型,成为DataContractSerializer的已知类型(Known Type)。DataContractSerializer内部具有一个已知类型的列表,我们需要将Order类型添加到这个列表中。对于已知类型,可以通过两个特性设置:KnownTypeAttribute和ServiceKnownTypeAttribute。KnownTypeAttribute应用于数据契约中,用于设置继承于该数据契约类型的子数据契约,或引用其他的契约类型。ServiceKnownTypeAttribute既可以应用于服务契约的接口和方法上,还可以应用在服务实现的类和方法上,应用在不同的目标元素,决定了定义已知类型的作用范围,下面,通过在基类OrderBase指定了子契约的类型Order:

   [DataContract]    [KnownType(typeof(Order))]    public abstract class OrderBase : IOrder    { ... }

  而ServiceKnownTypeAttribute特性,不仅可以使用在服务契约类型上,还可以应用在服务契约的操作方法上。如果应用在服务契约类型上,则已知类型在所有实现了该契约的服务操作中都有效,即作用范围为服务契约界别的,如果应用于服务契约的操作方法上,则定义的已知类型仅在实现了该契约的服务操作中有效。

  // 服务契约    [ServiceContract]    [ServiceKnownType(typeof(Order))]  // 服务契约级别    public interface IUserValidationService    {           // 为了演示已知类型的操作方法        //[OperationContract]        //[ServiceKnownType(typeof(Order))] // 单个服务操作级别        bool AddOrder(OrderBase order);    }

  除了通过特性的方式设置已知类型外,还可以通过配置文件的方式来进行指定。已知类型定义在<System.runtime.serialization>配置节点中,可以采用下面的方式来定义:

<configuration>  <system.runtime.serialization>    <dataContractSerializer>      <declaredTypes>        <add type="BusinessEntity.OrderBase,BusinessEntity.KnownTypes">          <knownType type="BusinessEntity.Order,BusinessEntity.KnownTypes"/>        </add>       </declaredTypes>    </dataContractSerializer>  </system.runtime.serialization></configuration>

六、总结

  到这里,数据契约的分享就结束。对于这篇博文首先介绍了数据契约和序列化的基本知识,接着介绍了.NET中的序列化机制和WCF中序列化机制,最后完成了一个数据契约的例子。看完本篇文章应该明确几个问题:

  1. SerializableAttribute与DataContract异同。

答:相同点:都是标记类型为可序列化类型

  不同点:在于序列化的成员不一样,DataContract是Opt-in(明确参与)的方式,即使用DataMember特性明确标识哪些成员需要序列化,而Serializable是Opt-out方式,即使用NoSerializable特性明确标识不参与序列化的成员。

  2. BinartFormatter、DataContractSerializer和XmlSerializer的区别,具体答案见下图和参考下面博文:XmlSerializer, DataContractSerializer 和 BinaryFormatter区别与用法分析。

  好的博文记录:Create and Consume RESTFul Service in .NET Framework 4.0

  本文所有源代码下载:WCFDataContract.zip

 

跟我一起学WCF(7)——WCF数据契约与序列化详解