服务版本控制

在初始部署后,并且在其生命周期内可能需要多次进行更改,服务(及其提供的终结点)可能需要出于各种原因进行更改,例如更改业务需求、信息技术要求或解决其他问题。 每个更改都会引入服务的新版本。 本主题介绍如何考虑 Windows Communication Foundation(WCF)中的版本控制。

四类服务更改

可能需要的服务的更改可分为四个类别:

  • 合同变化:例如,可能会添加操作,或者添加或更改消息中的数据元素。

  • 地址更改:例如,服务将移动到终结点具有新地址的其他位置。

  • 绑定更改:例如,安全机制更改或其设置更改。

  • 实现更改:例如,当内部方法实现发生更改时。

其中一些更改称为“中断”,而另一些则称为“非中断”。如果以前版本中成功处理的所有消息在新版本中成功处理,则更改是 非中断 性的。 不符合该条件的任何更改都为中断性更改

服务方向和版本控制

服务方向的一个原则是服务和客户端是自主的(或独立的)。 除此之外,这意味着服务开发人员不能假定他们控制甚至不知道所有服务客户端。 这消除了在服务更改版本时重新生成和重新部署所有客户端的选项。 本主题假定服务遵循此原则,因此必须进行更改或“版本化”,以使其独立于其客户端。

如果中断性变更意外且无法避免,应用程序可以选择忽略此原则,并要求使用新版本的服务重新生成和重新部署客户端。

协定版本管理

客户端使用的协定不需要与服务使用的协定相同;它们只需要兼容。

对于服务协议,兼容性意味着可以添加服务公开的新的操作,但不能删除或在语义上更改现有的操作。

对于数据协定,兼容性意味着可以添加新的架构类型定义,但不能以中断的方式更改现有架构类型定义。 中断性更改可包括移除数据成员或不兼容地更改其数据类型。 此功能允许服务在不中断客户端的情况下灵活更改其合约版本。 接下来的两部分说明可对 WCF 数据和服务协定进行的非中断性和中断性更改。

数据协定版本控制

本部分介绍使用 DataContractSerializerDataContractAttribute 类时的数据版本控制。

严格版本控制

在许多情况下,更改版本是个问题,服务开发人员对客户端没有控制权,因此无法对消息 XML 或架构中的更改做出反应。 在这些情况下,必须保证新消息将针对旧架构进行验证,原因有两个:

  • 旧客户端是使用假设架构不会更改而开发的。 他们可能无法处理那些不是为他们设计的消息。

  • 旧客户端可能会在尝试处理消息之前对旧架构执行实际架构验证。

此类方案中的建议方法是将现有数据协定视为不可变,并创建具有唯一 XML 限定名称的新协定。 然后,服务开发人员会将新方法添加到现有服务协定,或使用使用新数据协定的方法创建新的服务协定。

通常,服务开发人员需要编写一些业务逻辑,该逻辑应在数据协定的所有版本中运行,以及每个版本数据协定的版本特定的业务代码。 本主题末尾的附录介绍了如何使用接口来满足此需求。

宽松版本管理

在许多其他方案中,服务开发人员可以假设向数据协定添加新的可选成员不会中断现有客户端。 这要求服务开发人员调查现有客户端是否不执行架构验证,以及它们是否忽略未知数据成员。 在这些方案中,可以利用数据协定功能以非中断方式添加新成员。 如果用于版本控制的数据协定功能已用于服务的第一个版本,则服务开发人员可以放心地做出此假设。

WCF、ASP.NET Web 服务和其他许多 Web 服务堆栈都支持 宽松的版本控制:也就是说,它们不会为接收的数据中新的未知数据成员引发异常。

很容易错误地认为添加新成员不会破坏现有客户端。 如果不确定所有客户端都可以处理宽松的版本控制,建议使用严格的版本控制准则并将数据协定视为不可变。

有关数据协定宽松和严格版本控制的详细指南,请参阅 最佳做法:数据协定版本控制

区分数据协定和 .NET 类型

通过将特性应用于 DataContractAttribute 类,可以将 .NET 类或结构投影为数据协定。 .NET 类型及其数据协定投影是两个不同的问题。 可以有多个具有相同数据协定投影的 .NET 类型。 此区别尤其有益于允许在保留映射的数据协定的同时更改 .NET 类型,从而甚至在是严格意义上保持与现有客户端的兼容性。 应始终执行两项作来维护 .NET 类型和数据协定之间的这种区别:

  • 指定NameNamespace。 应始终指定数据协定的名称和命名空间,以防止 .NET 类型的名称和命名空间在协定中公开。 这样,如果稍后决定更改 .NET 命名空间或类型名称,则数据协定保持不变。

  • 指定 Name。 应始终指定数据成员的名称,以防止在协定中公开 .NET 成员名称。 这样,如果稍后决定更改成员的 .NET 名称,则数据协定保持不变。

更改或删除成员

更改成员的名称或数据类型或删除数据成员是一项中断性变更,即使允许宽松版本控制也是如此。 如有必要,请创建新的数据协定。

如果服务兼容性非常重要,可以考虑忽略代码中未使用的数据成员并保留这些成员。 如果要将数据成员拆分为多个成员,则可以考虑将现有成员保留为一个属性,该属性可以为下层客户端执行所需的拆分和重新聚合(未升级到最新版本的客户端)。

同样,对数据契约的名称或命名空间的更改属于破坏性变更。

未知数据的往返

某些情况下,需要“往返”传递来自于新版本中添加的成员的未知数据。 例如,“versionNew”服务会将一些新添加的成员的数据发送到“versionOld”客户端。 客户端在处理消息时会忽略新添加的成员,但它会将相同的数据(包括新添加的成员)重新发送回 versionNew 服务。 进行数据更新的典型场景是从服务中检索、修改并返回数据。

若要对某一特定类型启用往返功能,该类型必须实现 IExtensibleDataObject 接口。 接口包含一个属性ExtensionData,它返回ExtensionDataObject类型。 该属性用于存储当前版本未知的数据协定的未来版本中的任何数据。 此数据对客户端不透明,但在序列化实例时,该属性的内容将与数据协定成员数据的其余部分 ExtensionData 一起写入。

建议所有类型都实现此接口,以适应新的和未知的未来成员。

数据合约库

可能有一个数据协定库,其中协定发布到中央存储库,服务和类型实现者实现并从该存储库公开数据协定。 在这种情况下,向存储库发布数据协定时,你无法控制创建实现该协定的类型的人员。 因此,在发布合同后,无法修改该合同,使其实际上不可变。

使用 XmlSerializer 的场合

使用 XmlSerializer 类时,相同的版本控制原则适用。 如果需要严格的版本控制,请将数据协定视为不可变,并为新版本创建具有唯一限定名称的新数据协定。 确定可以使用宽松版本控制时,可以在新版本中添加新的可序列化成员,但不能更改或删除现有成员。

注释

XmlSerializer 使用 XmlAnyElementAttributeXmlAnyAttributeAttribute 属性来支持未知数据的往返。

消息契约版本控制

消息协定版本控制指南与版本控制数据协定非常相似。 如果需要严格的版本控制,则不应更改邮件正文,而是创建具有唯一限定名称的新邮件协定。 如果知道可以使用宽松版本控制,可以添加新的消息正文部件,但不能更改或删除现有部件。 本指南适用于裸露的和封装的消息合同。

始终可以添加消息标头,即使正在使用严格的版本控制也是如此。 MustUnderstand 标志可能会影响版本控制。 通常,WCF 中标头的版本控制模型如 SOAP 规范中所述。

服务协定版本控制

与数据协定版本控制类似,服务协定版本控制还涉及添加、更改和删除作。

指定名称、命名空间和动作

默认情况下,服务协定的名称是接口的名称。 其默认命名空间为 http://tempuri.org,每个操作的动作为 http://tempuri.org/contractname/methodname。 建议显式指定服务合同的名称和命名空间,并为每个操作指定一个动作,以避免使用 http://tempuri.org 并防止在服务合同中暴露接口和方法名称。

添加参数和操作

添加服务公开的服务操作是非中断性更改,因为现有客户端不需要考虑这些新操作。

注释

向双工回调协定添加操作是中断性更改。

操作参数或返回类型更改

更改参数或返回类型通常是一项中断性变更,除非新类型实现由旧类型实现的相同数据协定。 若要进行此类更改,请将新的操作添加到服务合同或定义新的服务合同。

移除操作

移除操作也是中断性更改。 若要进行此类更改,请定义新的服务协定,并在新终结点上公开它。

错误协定

FaultContractAttribute属性使服务合同开发人员能够指定有关合同操作中可能返回的错误的信息。

服务协定中描述的故障列表并不详尽。 操作可能随时返回其协定中未描述的错误。 因此,更改合同中描述的错误集合并不被视为违反。 例如,使用 FaultContractAttribute 向合同中添加新错误或从合同中删除现有错误。

服务合同库

组织可能有合同库,其中合同发布到中央存储库,并且服务实施者从该存储库执行和实现这些合同。 在这种情况下,将服务协定发布到存储库时,你无法控制谁创建了实现它的服务。 因此,一旦发布,就不能修改服务合同,从而实质上使其不可变。 WCF 支持协定继承,该继承可用于创建扩展现有协定的新协定。 若要使用此功能,请定义继承自旧服务协定接口的新服务协定接口,然后将方法添加到新接口。 然后,更改实现旧协定的服务以实现新协定,并将“versionOld”终结点定义更改为使用新协定。 对于“versionOld”客户端,终结点将仍然表现为公开“versionOld”协定;而对于“versionNew”客户端,终结点将表现为公开“versionNew”协定。

地址和绑定版本管理

终结点地址和绑定的更改是中断性变更,除非客户端能够动态发现新的终结点地址或绑定。 实现此功能的一种机制是使用通用发现描述和集成(UDDI)注册表和 UDDI 调用模式,其中客户端尝试与终结点通信,并在失败时查询当前终结点元数据的已知 UDDI 注册表。 然后,客户端使用此元数据中的地址和绑定来与终结点通信。 如果此通信成功,客户端将缓存地址和绑定信息以供将来使用。

路由服务和版本控制

如果对服务所做的更改是中断性变更,并且需要同时运行两个或更多不同版本的服务,则可以使用 WCF 路由服务将消息路由到相应的服务实例。 WCF 路由服务使用基于内容的路由,换句话说,它使用消息中的信息来确定消息的路由位置。 有关 WCF 路由服务的详细信息,请参阅 路由服务。 有关如何使用 WCF 路由服务进行服务版本控制的示例,请参阅 How To: Service Versioning

附录

需要严格版本控制时的一般数据协定版本控制指南是将数据协定视为不可变,并在需要更改时创建新协定。 需要为每个新数据协定创建新类,因此需要一种机制,以避免采用以旧数据协定类编写的现有代码,并在新的数据协定类中重写它。

其中一种机制是使用接口来定义每个数据协定的成员,并在接口而不是实现接口的数据协定类方面编写内部实现代码。 服务版本 1 的以下代码显示了 IPurchaseOrderV1 接口和 PurchaseOrderV1

public interface IPurchaseOrderV1  
{  
    string OrderId { get; set; }  
    string CustomerId { get; set; }  
}  
  
[DataContract(  
Name = "PurchaseOrder",  
Namespace = "http://examples.microsoft.com/WCF/2005/10/PurchaseOrder")]  
public class PurchaseOrderV1 : IPurchaseOrderV1  
{  
    [DataMember(...)]  
    public string OrderId {...}  
    [DataMember(...)]  
    public string CustomerId {...}  
}  

虽然服务合同的操作将用 PurchaseOrderV1 表示,但实际业务逻辑将用 IPurchaseOrderV1 表示。 然后,在版本 2 中,会有一个新的 IPurchaseOrderV2 接口和一个新 PurchaseOrderV2 类,如以下代码所示:

public interface IPurchaseOrderV2  
{  
    DateTime OrderDate { get; set; }  
}

[DataContract(
Name = "PurchaseOrder",  
Namespace = "http://examples.microsoft.com/WCF/2006/02/PurchaseOrder")]  
public class PurchaseOrderV2 : IPurchaseOrderV1, IPurchaseOrderV2  
{  
    [DataMember(...)]  
    public string OrderId {...}  
    [DataMember(...)]  
    public string CustomerId {...}  
    [DataMember(...)]  
    public DateTime OrderDate { ... }  
}  

该服务协定将更新为包含按照 PurchaseOrderV2 编写的新操作。 现有基于IPurchaseOrderV1编写的业务逻辑将继续适用于PurchaseOrderV2,而需要OrderDate属性的新业务逻辑将基于IPurchaseOrderV2编写。

另请参阅