在WCF应用程序中处理变更的最佳方法
 

2009-07-06 作者:Sunny 来源:IT专家网

 

定义变更

在进行如何处理变更之前,需要解释一下在基于WCF服务中变更是什么意思。以下列出构成变更的一些行为(按照契约类型列出):

数据契约

添加数据成员

删除数据成员

重命名数据成员

改变数据成员类型

服务契约

添加操作

删除操作

重命名服务契约

操作契约

重命名操作

改变操作签名

这些变更可能会导致新的商业需求,硬件整合,业务合并,新的规则或者一些其他的外部因素。底线是在开发者的控制变更以外的一些东西,并且软件必须进行调整。在WCF世界中处理变更是好消息或坏消息不断出现的过程。你可以很容易的处理一些方案,而其它的会引导你给出"yes, but…"这样可怕的回应。

在WCF中的版本控制和变更

在.NET世界中,处理变更第一个需要考虑的问题就是版本控制。你可以组合版本允许在以后组件的修订版本中有意料之外的或是终止的变更。这样以来,受到影响的客户可以继续使用旧版本的组合,你也可以避免因为这终止性的变更而感到头疼。

正常合乎逻辑的问题是“WCF支持版本控制吗?”回答也是那个可怕的"yes, but…"。当你在WCF中创建一个数据契约的时候,该契约会产生一个XML构架。Consumers引用这个构架并使用它生成一个代理类。严格的说,这个数据不能验证这个构架是否能继续进行。但是你将会看到,这个有时候会对导致服务 consumers一些意外的而且是令人沮丧的行为。

在进入一些细节之前,熟悉以下的示例服务。它为本文余下部分的讨论提供了基础。

      namespace SampleService

  {

  [ServiceContract]

  public interface IPersonService

  {

  [OperationContract]

  Person GetPerson(int personId);

  [OperationContract]

  void UpdatePerson(Person p);

  }

  public class Person

  {

  private string _firstName = string.Empty;

  private string _lastName = string.Empty;

  [DataMember]

  public string FirstName

  {

  get { return _firstName; }

  set { _firstName = value; }

  }

  [DataMember]

  public string LastName

  {

  get { return _lastName; }

  set { _lastName = value; }

  }

  }

  }

数据契约变更

Person DataContract 定义了两个属性:FirstName 和LastName。如果客户端引用了这个服务,而你后来将LastName改成SurName,实际上客户端不会中断,但是在客户端的代理类上的LastName属性会显示为空的。这是因为当客户端解串信息到Person类的时候,不会找到任何叫做Lastname的元素。

这个简单的变更不会引起客户端的错误,但它会导致更严重的问题:一个意外的行为。这个错误很容易跟踪,但是跟踪变更行为是非常困难的。

这个简单的例子说明为什么任何服务变更和它们后续的影响是至关重要的。除非你自己知道利用你的web服务的每个客户端应用程序,否则变更会是个灾难。作为一名开发人员,你需要尽一切努力使你的客户端避免变更的出现。

首先,你可以应用一些最佳的方法来帮助客户端免于内部变更。数据契约的更新版本如以下所示:

      [DataContract(Namespace="http://types.mycompany.com/2009/05/25", Name="PersonContract")]

  public class Person : IExtensibleDataObject

  {

  private string _firstName = string.Empty;

  private string _lastName = string.Empty;

  private ExtensionDataObject _extensionData;

  [DataMember(Name="FirstName")]

  public string FirstName

  {

  get { return _firstName; }

  set { _firstName = value; }

  }

  [DataMember(Name="LastName")]

  public string LastName

  {

  get { return _lastName; }

  set { _lastName = value; }

  }

  public ExtensionDataObject ExtensionData

  {

  get { return _extensionData; }

  set { _extensionData = value; }

  }

  }

在DataContract上增加的Namespace, Name和Order参数以及DataMember属性控制DataContractSerializer的行为。当服务的reference被添加的时候,这个增加的东西生成客户端代理。Name参数让serializer使用所指出的值,而不是实际公布的成员或是属性的名字。这个方法在没有影响客户端的情况下允许变更的内部执行。例如,考虑以下的变更:

      [DataMember(Name="LastName")]

  public string SurName

  {

  get { return _lastName; }

  set { _lastName = value; }

  }

将"FirstName" 改成"SurName"的属性名字变更不会中断现有的客户端,因为客户端使用的Name参数仍然是"FirstName."。只是内部执行被该改变了。 

第二个显著的变化是增加了IExtensibleDataObject接口。执行这个接口允许客户端保留在契约中没有明确定义的数据。这似乎看上去没什么用,但是在客户端希望执行示例Person对象并返回它的情况下,客户端可以保留新的数据项目。例如,用以下不会强迫现有的客户端进行更新的新成员来更新PersonContract:

      [DataMember(Name = "MiddleName", Order = 3)]

  public string SurName

  {

  get { return _middleName; }

  set { _middleName = value; }

  }

实际上,这个成员通过往返过程的服务,允许现存的客户端保留放在"MiddleName"中的值。执行IExtensibleDataObject对你的数据契约未来的论证是一个有益的方法。 

请记住,实际上客户端对一个外部构架有验证信息的选择权。(对WCF应用程序添加信息构架验证的详细讨论,请阅读此文章this article)。因此,当处理数据契约的变更时你有两种情况需要考虑:有构架验证和没有构架验证。

当客户端添加构架验证的时候,在数据契约中进行添加,变更或是减去任何项目将导致验证的失败。所以,在严格的构架验证被使用的情况下,不能对契约进行改变。相反,你需要创建一个完整的新的契约并在这个契约中使用不同的命名空间来显示新版本。

例如,从执行的角度来看,你需要两个单独的服务终端使这两个版本可用:

      Original Version: [DataContract(Namespace="http://schemas.mycompany.com/2009/05/25")]

  New Version: [DataContract(Namespace=http://schemas.mycompany.com/2009/06/18)]

幸运的是,严格的构架验证不是默认的行为。这意思是在没有中断服务端的情况下,你可以添加或删除数据成员。但是,由于先前所讨论的有不预期的行为会产生,删除数据成员不是一个好主意。另一方面,添加一个数据成员是很容易做到的,而且consumers会忽略它们还没有意识到的外部成员。 

关键的做法是使用DataMember属性(先前讨论过的)的Order参数。使用这个参数可以告诉serializers什么样的顺序(每个成员的)可以出现在XML中

来自基础类型的成员

没有order参数(按字母顺序排列)的成员

有order参数(按值排列)的成员

数据契约考虑的最后情况是改变一个数据成员的类型。这种情况下,最好的方法是创建一个新版本的数据契约并带有新的服务契约,执行和终端。

服务契约变更

所有的服务契约需要遵循在ServiceContract属性上使用Name 和 Namespace参数的最佳方法。一个更新版本的Person服务契约是像这样的:
      [ServiceContract(Name="PersonService", Namespace="http://services.mycompany.com/2009/05/25"]

  public interface IPersonService

同数据契约一样,使用Name从实际的接口名字中孤立服务consumer,需要的时候允许内部变更执行。所添加的Namespace允许你在未来的某一时刻进行契约的版本控制。记住新版本也需要新的终端。 

在没有中断现有的consumers的情况下,你可以添加服务契约的操作。Consumers会忽略这些新的操作。另一方面,删除操作将中断现有的consumers。像所有的中断变更一样,操作的删除需要带有新终端的新版本。

操作契约变更

服务和数据契约都有了,你需要使用带有OperationContract 属性的Name参数:

[OperationContract(Name="GetPerson"]

Person GetPerson(int personId);

Consumer再次从内部执行变更中脱离。

最后需要考虑的变更是操作契约的签名。这是个终止变更,你有两个解决方案:创建一个新版本或是添加一个新操作到服务契约上。

忠于你的承诺

变更是不可避免的,如果这个变更是有计划的,还带有一些主要的原则,那么你可以减少因变更对你的WCF 服务consumers的影响。永远记住当你要发布一个服务的时候,你要做出承诺让服务consumers忠于契约。对现有的契约进行变更是很不好的形式。

最后,请记住这些最好的做法:

总是在所有契约上使用Name和 Namespace参数。

总是在数据成员上使用Order参数

在所有数据契约上执行IExtensibleDataObject。

对契约版本控制使用命名空间。

总是记住新版本需要新终端。

当使用严格的构架验证的时候,不要改变契约。创建新版本。

当从服务契约中删除操作的时候,创建一个新版本。

当改变一个操作的签名的时候,创建新版本。

记住这些实践方法,你就可以很容易的为你自己或你的服务consumers处理变更。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织