数据契约

简介:

《Programming WCF Services》翻译笔记之三

本书的第3章主要讲解了有关数据契约的知识。“从抽象层面看,WCF能够托管CLR类型(接口和类) 并将它们公开为服务,也能够以本地CLR接口和类的方式使用服务。WCF服务的操作接收和返回诸如int和string的CLR类型,WCF客户端则传递 和处理返回的CLR类型。然而,CLR类型却属于.NET的特定技术。由于面向服务的一个核心原则就是在跨越服务边界时,服务不能够暴露它们的实现技术。 因此,不管客户端采用了何种技术,它都能够与服务交互。显然,这就意味着WCF不允许在跨越服务边界时公开CLR数据类型。我们需要找到一种办法,实现 CLR数据类型与标准的与平台无关的表示形式之间的转换。这样的表示形式就是基于XML的样式或信息集(Infoset)。此外,服务需要一种正式的方法 声明两者之间的转换。这个方法就是本章所要介绍的主题——数据契约。本章的第一部分介绍了数据契约启用类型封送(Type Marshaling)与转换的方法,以及如何通过基础架构处理类的层级与数据契约的版本控制。第二部分则介绍了如何将不同的.NET类型,例如枚举、委 托、数据表以及集合,作为数据契约使用。”

数据契约是服务之间传递的数据。由于必须支持跨进程,乃至于跨机器的传递,WCF必须对数据进行特殊的处理,否则无法实现数据的传递。在.NET Remoting与Web Service中,对数据的处理方式通常是利用序列化的方法,WCF同样沿袭了这一做法,但为了更好的体现面向服务的特质,又特别引入了数据契约 (DataContract)。此外,WCF还引入了消息契约(MessageContract),但本书没有介绍。

WCF的序列化使用了.NET平台自身支持的序列化机制,因此这里不再重复。

.NET提供的序列化机制虽然足以应付SOA的要求,但仍然存在许多不足之处。本书总结了Serializable的缺陷:
“Serializable 所指代的涵义是类型的所有成员都是可序列化的,这些成员是组成类型数据样式的一部分。然而,更好的方式是能够提供一种明确参与(Opt-In)途径,只有 那些契约的开发者明确包含的成员才应该放到数据契约中。Serializable特性强制要求数据类型是可序列化的,从而使得类型可以被用作契约操作的参 数,但它却无法实现类型的服务特性(具有成为WCF操作参数的能力)与序列化能力之间的职责分离。Serializalbe特性不支持类型名和成员名的别 名,也无法将一个新类型映射为预定义的数据契约。由于Serializable特性可以直接操作成员字段,使得封装了字段访问的属性形同虚设。访问字段的 最好办法是通过属性添加它们的值,而Serializable却破坏了属性的封装性。最后,Serializable特性并没有直接支持版本控制 (Versioning),而版本控制的信息却是格式器期望获取的。无疑,它导致了版本控制的处理变得举步维艰。”

WCF提供的数据契约DataContract基本上解决了以上的问题。通常,DataContract必须与DataMember结合使用。只有 应用了DataMember特性的属性才被公开到元数据中。虽然DataMember特性也可以应用到对象的字段上,但WCF并不推荐这样做,原因与类的 设计原则相同。

数据契约与服务契约相似,数据成员或数据契约的访问限定与WCF之间并没有因果关系。数据契约完全可以包含私有数据成员等内部类型:

[DataContract]
struct  Contact
{
   [DataMember]
    string m_FirstName;

   [DataMember]
    string m_LastName;
}

即使DataMember特性被直接应用到字段上,在导入的客户端定义仍然会以属性来表示。如下的数据契约定义:

[DataContract]
struct  Contact
{
   [DataMember]
    public  string FirstName;

   [DataMember]
    public  string LastName;
}

导入的客户端定义为:

[DataContract]
public  partial  struct  Contact
{
    string FirstNameField;
    string LastNameField;

   [DataMember]
    public  string FirstName
    {
       get
       {
          return FirstNameField;
       }
       set
       {
         FirstNameField =  value;
       }
    }

   [DataMember]
    public  string LastName
    {
       get
       {
          return LastNameField;
       }
       set
       {
         LastNameField =  value;
       }
    }
}

它会将字段名作为属性名,而导入的定义中,则在属性名后加上Field后缀作为字段名。但我们也可以手工修改客户端的定义。

如果数据契约的数据成员为私有的,导入的客户端定义会自动修改为公有的。“当DataMember特性应用到属性上时(不管是服务还是客户端),该 属性必须具有get和set访问器。如果没有,在调用时就会抛出InvalidDataContractException异常。因为当属性自身就是数据 成员时,WCF会在序列化和反序列化时使用该属性,使开发者能够将定制逻辑应用到属性中。”

“不要将DataMember特性既应用到属性上,又应用到相对应的字段上,这会导致导入的成员定义重复。”

如果服务端的数据被标记为Serializable特性,在导入这样的定义时,会使用DataContract。而且“对于每一个可序列化的成员,不管是公有的还是私有的,都是数据成员。” 

传统的格式器不能序列化只标记了DataContract特性的类型。要序列化这样的类型,必须同时应用DataContract特性和 Serializable特性。对于如此类型生成的传输型表示形式(Wire Representation),就好似仅仅应用了DataContract特性一般,同时,我们仍然需要为成员添加DataMember特性。

在WCF的数据契约中,很明显地体现出WCF还不能够完全支持面向对象的设计思想。在第2章对服务契约的描述中,对契约的继承层级的处理方式来看,已经体现了这一缺陷的端倪。而对于数据契约而言,更是进一步暴露了这样的缺陷。

首先WCF并不支持Liskov替换原则(LSP),“默认情况下,我们不能用数据契约的子类去替换基类。” 考虑如下的服务契约:

[ServiceContract]
interface IContactManager
{
    //Cannot accept Customer object here:
   [OperationContract]
    void AddContact(Contact contact);

    //Cannot return Customer objects here:
   [OperationContract]
   Contact[] GetContacts(  );
}

假定客户端同时定义了一个Customer类:

[DataContract]
class  Customer : Contact
{
   [DataMember]
    public  int OrderNumber;
}

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

Contact contact =  new Customer(  );
contact.FirstName =  "Juval";
contact.LastName =  "Lowy";

ContactManagerClient proxy =  new ContactManagerClient(  );
//Service call will fail:
proxy.AddContact(contact);
proxy.Close(  );

因为在这个例子中,我们传递了一个Customer对象,而不是Contact对象。由于服务无法识别Customer对象,也就无法反序列化它所接收到的Contact对象。

虽然WCF引入了Known Types(已知类型)来解决这一问题,然而对于理解面向对象思想的设计者而言,这样的设计无疑会引入父类与子类之间的耦合度。因为在我们设计父类的时候,就必须事先知道子类的定义。当我们需要扩展子类时,还需要修改父类的定义。

WCF引入的服务已知类型,比较已知类型而言,有一定程度的改善。因为它可以将父类与子类在层级上的耦合度缩小到方法级上。但这样的耦合,依然是不可接受的。例如:

[DataContract]
class  Contact
{... }

[DataContract]
class  Customer : Contact
{... }

[ServiceContract]
interface IContactManager
{
   [OperationContract]
   [ServiceKnownType(typeof(Customer))]
    void AddContact(Contact contact);

   [OperationContract]
   Contact[] GetContacts(  );
}

当然,服务已知类型也可以应用到契约接口上,此时,该契约以及实现该契约的所有服务包含的所有操作都能够接收已知的子类。

为了解决这一问题,WCF提供了配置已知类型的方法。例如:

<system.runtime.serialization>
   <dataContractSerializer>
      <declaredTypes>
         <add type =  "Contact,Host,Version=1.0.0.0,Culture=neutral,
                                                              PublicKeyToken=null">
            <knownType type =  "Customer,MyClassLibrary,Version=1.0.0.0,
                                             Culture=neutral,PublicKeyToken=null"/>
         </add>
      </declaredTypes>
   </dataContractSerializer>
</system.runtime.serialization>

注意上述的配置文件中,我们配置的已知类型必须是类型的fullname。包括命名空间、版本号、Culture等。虽然这种方式可以避免在增加子类的情况下,修改代码、重新编译和重新部署,但无疑加重了开发者的负担,尤其是对配置文件的管理以及后期的维护。

不过,“如果已知类型对于另一个程序集而言是内部(internal)类型,要添加一个已知类型,只有使用配置文件声明它。”

总之,在WCF中要实现面向对象的多态,还未能做到最佳。如果能够将KnownType特性应用到子类上,为子类指名它所继承的父类,无疑更加利于类的扩展。遗憾的是WCF未能做到这一点。

如果数据契约本身实现了一个接口,情况就变得有趣了。从服务端的定义来看,这样的数据契约仍然可以通过服务已知类型在服务契约上指定实现了数据契约接口的子数据契约类型。例如,数据契约Contact类实现了接口IContact:

interface IContact
{  
    string FirstName
    {get; set; }
    string LastName
    {get; set; }
}
[DataContract]
class  Contact : IContact
{
     //...
}

那么在处理数据契约Contact的服务契约中,如果契约的操作需要以抽象方式,定义IContact类型的参数,就必须使用ServiceKnownType特性指名其实现类Contact,如下所示:

[ServiceContract]
[ServiceKnownType(typeof(Contact))]
interface IContactManager
{
   [OperationContract]
    void AddContact(IContact contact);

   [OperationContract]
   IContact[] GetContacts(  );
}

注意,此时不能利用KnownType特性,将其直接应用到IContact接口上,因为导出的元数据无法包含接口本身。

服务端的定义无疑符合面向接口编程思想,除了增加了ServiceKnownType之外,整个设计还算优雅。然而根据这样的定义所导出的服务契约,却未免显得差强人意,如下所示:

[ServiceContract]
public  interface IContactManager
{
    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    [ServiceKnownType(typeof(object[]))]
     void AddContact( object contact);
   
    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    [ServiceKnownType(typeof(object[]))]
     object[] GetContacts(  );
}

导出定义中,将应用到契约的ServiceKnownType特性应用到了每个操作上,并且为每个操作都指定了具体的数据契约子类以及一个 object[]类型。特别要注意,在操作的返回值与参数中,原来的IContact类型全部被转换为了object类型。原因在于,客户端并没有 IContact接口的定义。基于object的契约定义无疑不具备类型安全性。

解决办法自然是在客户端中增加IContact接口的定义。如此,客户端定义就可以修改为:

[ServiceContract]
public  interface IContactManager
{
    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
     void AddContact(IContact contact);
   
    [OperationContract]
    [ServiceKnownType(typeof(Contact))]
    IContact[] GetContacts(  );
}

但是,我们不能以具体的数据契约类型Contact,来替换原来的object类型。因为替换为具体的数据契约类型,则客户端的服务契约就与服务端的服务契约不兼容了。所以,下面的定义是错误的:

[ServiceContract]
public  interface IContactManager
{
    [OperationContract]
     void AddContact(Contact contact);
   
    [OperationContract]
    Contact[] GetContacts(  );
}

随着版本的不断演化,客户端与服务端的数据契约可能会出现版本不一致的情况。在WCF中,关于数据契 约的版本控制有两种情况:新增成员与缺失成员。新增成员是指发送方包含了新增成员,默认处理方式为忽略新增成员。缺失成员则是指发送方缺少成员,默认处理 方式是为缺失成员赋予其默认值。

在缺失成员的情况下,如果仅仅是为缺少的成员赋予默认值,有时候会出现无法预料的错误。原因在于缺失的成员有可能是正确执行操作的必要条件。为了避 免出现这样的情况,可以将缺失的成员设置为必备成员,方法是利用DataMember特性的IsRequired属性,将其值设置为true。例如:

[DataContract]
struct  Contact
{
   [DataMember]
    public  string FirstName;
  
   [DataMember]
    public  string LastName;

   [DataMember(IsRequired = true)]
    public  string Address;
}

如果消息中的成员被标记为必备成员,当接收端的DataContractSerializer无法找到所需的信息进行反序列化时,就会取消这次调 用,发送端会引发NetDispatcherFaultException异常。例如,服务端的数据契约如上的定义,其中Address字段为必备成员, 而客户端的数据契约则如下所示:

[DataContract]
struct  Contact
{
   [DataMember]
    public  string FirstName;
  
   [DataMember]
    public  string LastName;
}

此时,如果客户端向服务发出调用,则由于引发了异常,该调用就不会到达服务。

客户端和服务都能够将它们的数据契约中的部分或所有数据成员标记为必备,彼此之间是完全独立的。被标记为必备的成员越多,则与服务或客户端之间的交互就越安全,但这却是以牺牲灵活性与版本兼容性为代价的。

本书总结了数据契约版本控制的几种情形,并以表显示了必备成员的版本兼容性:

IsRequired

V1 to V2

V2 to V1

False

Yes

Yes

True

No

Yes

假定V2包含了V1的所有数据成员,同时还定义了新增成员。则表涵盖了版本的几种情况。
V1到V2:代表了缺失成员的情况。如果IsRequired为false,则交互正常,对于缺失成员则设置为默认值。如果IsRequired为true,就会抛出异常,消息不能正常发送。
V2到V1:代表了新增成员的情况。不管IsRequired的值为true还是false,WCF均以忽略新成员的方式进行交互,交互正常。

WCF对于一些特殊的数据类型,支持仍然不够。这在一定程度上限制了CLR开发人员对WCF的设计。这些特殊的数据类型包括:枚举、委托、DataSet和DataTable、泛型、集合。

枚举

WCF对枚举的支持还算不错。首先,枚举类型自身是支持序列化的。不需要设置任何特性,枚举类型的所有成员都会是数据契约的一部分。如果,枚举类型中只有一部分成员需要成为数据契约的一部分,就需要用到DataContract与EnumMember特性。例如:

[DataContract]
enum ContactType
{
   [EnumMember]
   Customer,

   [EnumMember]
   Vendor,

    //Will not be part of data contract
   Partner
}

客户端生成的表示形式则为:

enum ContactType
{
   Customer,
   Vendor
}

委托

WCF对委托以及事件的支持都不够好。这是因为委托的内部调用列表的具体结构是本地的,客户端或服务无法跨服务边界共享委托列表的结构。此外,我们 不能保证内部列表中的目标对象都是可序列化的,或者都是有效的数据契约。这会导致序列化的操作时而成功,时而失败。因此,最佳实践是不要将委托成员或事件 作为数据契约的一部分。

数据集与数据表

DataSet和DataTable类型是可序列化的,因而我们可以在服务契约中接收或返回数据表或数据集。

如果服务契约使用了DataSet和DataTable类型,生成的代理文件不会直接使用DataSet和DataTable类型,而是包含 DataTable数据契约的定义(只包含DataTable的样式,而不包含任何代码)。但我们可以手工修改这些定义。例如这样的服务契约:

[ServiceContract()]
public  interface IContactManager
{
    [OperationContract]
     void AddContact(Contact contact);

    [OperationContract]
     void AddContacts(DataTable contacts);

    [OperationContract]
    DataTable GetContacts();
}

那么生成的代理文件可能会是这样:

public  interface IContactManager
{
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/AddContact", ReplyAction="http://tempuri.org/IContactManager/AddContactResponse")]
    [System.ServiceModel.XmlSerializerFormatAttribute()]
     void AddContact(Contact contact);
   
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/AddContacts", ReplyAction="http://tempuri.org/IContactManager/AddContactsResponse")]
    [System.ServiceModel.XmlSerializerFormatAttribute()]
    AddContactsResponse AddContacts(AddContactsRequest request);
   
    [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/GetContacts", ReplyAction="http://tempuri.org/IContactManager/GetContactsResponse")]
    [System.ServiceModel.XmlSerializerFormatAttribute()]
    GetContactsResponse GetContacts(GetContactsRequest request);
}

代理类的定义则如下所示:

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public  partial  class  ContactManagerClient : System.ServiceModel.ClientBase<IContactManager>, IContactManager
{
     //其余成员略;
   
     public  void AddContact(Contact contact)
     {
         base.Channel.AddContact(contact);
     }
   
    AddContactsResponse IContactManager.AddContacts(AddContactsRequest request)
     {
         return  base.Channel.AddContacts(request);
     }
   
     public  void AddContacts(AddContactsContacts contacts)
     {
        AddContactsRequest inValue =  new AddContactsRequest();
        inValue.contacts = contacts;
        AddContactsResponse retVal = ((IContactManager)( this)).AddContacts(inValue);
     }
   
    GetContactsResponse IContactManager.GetContacts(GetContactsRequest request)
     {
         return  base.Channel.GetContacts(request);
     }
   
     public GetContactsResponseGetContactsResult GetContacts()
     {
        GetContactsRequest inValue =  new GetContactsRequest();
        GetContactsResponse retVal = ((IContactManager)( this)).GetContacts(inValue);
         return retVal.GetContactsResult;
     }
}

我们可以手动将AddContacts()与GetContacts()方法修改为:

public  void AddContacts(DataTable contacts)
{
    AddContactsRequest inValue =  new AddContactsRequest();
    inValue.contacts = contacts;
    AddContactsResponse retVal = ((IContactManager)( this)).AddContacts(inValue);
}
public DataTable GetContacts()
{
    GetContactsRequest inValue =  new GetContactsRequest();
    GetContactsResponse retVal = ((IContactManager)( this)).GetContacts(inValue);
     return retVal.GetContactsResult;
}

当然,前提条件是我们需要修改AddContactRequest类以及GetContactsResponse,例如将 AddContactRequest类的contacts成员由原来的AddContactsContacts类型修改为DataTable类型;将 GetContactsResponse中的GetContactsResult成员由原来的 GetContactsResponseGetContactsResult类型修改为DataTable类型。

自动生成的代理类非常复杂,实际上我们完全可以简化。首先将客户端的服务契约定义修改为与服务端服务契约完全一致的定义:

[ServiceContract()]
public  interface IContactManager
{
    [OperationContract]
     void AddContact(Contact contact);

    [OperationContract]
     void AddContacts(DataTable contacts);

    [OperationContract]
    DataTable GetContacts();
}

然后修改代理类ContactManagerClient:

public  partial  class  ContactManagerClient : System.ServiceModel.ClientBase<IContactManager>, IContactManager
{   
     public  void AddContact(Contact contact)
     {
         base.Channel.AddContact(contact);
     }   
     public  void AddContacts(DataTable contacts)
     {
         base.Channel.AddContacts(contacts);
     }
     public DataTable GetContacts()
     {
         return  base.Channel.GetContacts();
     }
}

修改后运行的结果完全相同。

注意,DataRow类型是不能序列化的。

在WCF中,还可以使用DataTable和DataSet的类型安全的子类。书中也给出了相应的例子。然而,WCF的最佳实践则是避免使用DataTable和DataSet,以及使用DataTable和DataSet的类型安全的子类。书中阐释了原因:
“ 对于WCF的客户端与服务而言,虽然可以通过ADO.NET和Visual Studio工具使用DataSet、DataTable以及它们的类型安全的派生对象,但这种方式过于繁琐。而且,这些数据访问类型都是特定的.NET 类型。在序列化时,它们生成的数据契约样式过于复杂,很难与其它平台进行交互。在服务契约中使用数据表或者数据集还存在一个缺陷,那就是它可能暴露内部的 数据结构。同时,将来对数据库样式的修改会影响到客户端。虽然在应用程序内部可以传递数据表,但如果是跨越应用程序或公有的服务边界发送数据表,却并非一 个好的主意。通常情况下,更好的做法是暴露数据的操作而非数据本身。”

最好的做法是将DataTable转换为数组类型。书中提供了DataTableHelper类,可以帮助将DataTable转换为数组类型。

泛型

非常遗憾,我们并不能在数据契约中定义泛型。但是,WCF使用了一个折中的办法,使得我们可以在服务端照常使用泛型,但在生成的数据契约定义时,泛型会被具体的类型所取代,重命名的格式为:
<原有名>Of<类型参数名><哈希值>

WCF还支持将自定义类型作为泛型参数。此外,还可以通过数据契约的Name属性为导出的数据契约指定不同的名字。例如,如下的服务端数据契约:

[DataContract]
class  SomeClass
{... }

[DataContract(Name = "MyClass")]
class  MyClass<T>
{... }

[OperationContract]
void MyMethod(MyClass<SomeClass> obj);

导出的数据契约为:

[DataContract]
class  SomeClass
{... }

[DataContract]
class  MyClass
{... }

[OperationContract]
void MyMethod(MyClass obj);

集合

WCF支持泛型集合、定制集合,但与传统的.NET编程不一样,WCF对集合的操作存在许多约束。对于这些约束,本书描述得非常清楚。本文不再赘述。







本文转自wayfarer51CTO博客,原文链接:http://blog.51cto.com/wayfarer/280112,如需转载请自行联系原作者

相关文章
|
2月前
|
JSON 测试技术 API
契约测试
契约测试
WCF基础教程(四)——数据契约实现传送自定义数据类型
WCF基础教程(四)——数据契约实现传送自定义数据类型
83 0
译MassTransit 消息契约
消息契约 在MassTransit中,使用.NET .NET系统定义消息契约。消息可以使用类和接口来定义,但是,建议类型使用只读属性而不使用行为。 注意:强烈建议使用消息接口的接口,基于多年的经验,具有不同级别的开发经验。
1055 0
|
C# 网络架构 C++
|
网络架构 Windows
WCF 设计和实现服务协定(01)
作者:jiankunking 出处:http://blog.csdn.net/jiankunking WCF 术语: • 消息 – 消息是一个独立的数据单元,它可能由几个部分组成,包括消息正文和消息头。• 服务 – 服务是一个构造,它公开一个或多个终结点,其中每个终结点都公开一个或多个服务操作。• 终结点 – 终结点是用来发送或接收消息(或执行这两种操作)的构造。 终结点包括一个定义消息可以
1227 0