向后兼容的服务接口,无需更新 azure service fabric 中的旧客户端
Backwards compatible service interfaces without updating older clients in azure service fabric
我有一个服务结构有状态服务,它公开了一个接口,如:
public interface IAction
{
Task GetCustomer (Customer customer)
}
客户 class 看起来像
[DataContract]
public class Customer
{
[DataMember]
public string Id {get;set;}
[DataMember]
public string Name {get;set;}
}
我现在已经通过 nuget 与服务的客户端共享了包含上述模型和接口的程序集。
一段时间后,我需要为其他客户更新客户 class,因此我通过添加额外的可为空 属性
来执行以下操作
[DataContract]
public class Customer
{
[DataMember]
public string Id {get;set;}
[DataMember]
public string Name {get;set;}
[DataMember]
public ulong? Salary {get;set;}
}
因为我已经添加了一个可为空的数据成员,所以我假设我只需要与新客户共享这个新模型和合同,而第一个客户不需要更新。
但是,我注意到出现以下异常:
{"Interface id 'xxxxxxxx' is not implemented by object '**'"}
在阅读了多个 SO 答案(,) 之后,我得出的结论是,客户必须始终准确参考当前 运行 版本中的接口和模型服务。
这是一个很大的限制,因为我不应该被迫更新所有客户端。添加的额外可选参数不应强制更新旧客户端,尤其是在服务可以保证完全向后兼容性的情况下。
有没有办法解决以向后兼容的方式更新服务接口而不必更新旧客户端的问题?
这是remoting的一个限制,调用者和发送者必须清楚地理解同一个消息(版本)才能进行通信,如果彼此不理解,就会抛出这个异常。
如果您必须具有这种灵活性,您可以:
This is quite a big limitation, as I should not be forced to update
all clients. Extra optional parameters added should not force the old
clients to be updated, especially if the service can guarantee full
backwards compatibility.
这根本不是限制,因为第一,服务不应该知道客户端,第二,添加 operations/members 不被视为破坏,因为客户端不需要知道添加。
服务中的合同变更被认为是“非破坏性”。您共享的 SO 上其他类似问题的链接并不能完全解决您的问题,可以将其归类为以下要点:
- 服务发现
- 服务版本控制
- 实施 IExtensibleDataObject(如果问题是由 data/data 类型在服务的新添加往返之后引起的)
1 和 2 是关于客户端正确发现服务,使用你的服务的客户端需要知道它 松散的版本控制 兼容性,即 您需要确认客户端在调用您的服务之前没有对旧服务执行任何模式验证。如果是这种情况,那么您需要使用 显式 XML 命名空间 并定义新契约和新服务定义。
这不是限制,但与严格版本控制保持一致,而且它还涉及早期客户端绑定未知的数据类型您的服务因回调而出现异常故障,这是合理的,您应该接受它与 SOA 无关。
要使用此解决方案,您可能需要按以下方式定义合同和服务:
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 {...}
}
和新添加的成员的另一个版本,
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 { ... }
}
关于这段代码的来源,您可以参考这个link,这一定会帮助您了解您的服务有什么问题以及如何修改它。
只是事后将此添加到 DataMember 的 isRequired 属性,默认情况下为 false。
以下引用自here。
If a default value of null or zero for the member is unacceptable, a
callback method should be provided using the
OnDeserializingAttribute
to provide a reasonable default in case the
member is not present in the incoming stream.
[OnDeserialized]
public void OnDeserialized(StreamingContext context)
{
if (this.id == null) throw new ArgumentNullException("id");
if (this.Name == null) throw new ArgumentOutOfRangeException("name");
if (this.Salary < 0) throw new ArgumentOutOfRangeException("salary");
if (this.Salary > 0)
{
throw new InvalidOperationException("No child labor allowed");
}
}
要支持具有相同远程服务的多个客户端,您需要两个不同的定义明确的 contracts/interfaces。这是根据服务结构中远程处理的设计。
通常,您应该只允许远程处理结构集群内的服务通信。原因是因为
A - DataContractSerializer 与服务结构紧密耦合。而且我假设您的客户不知道或不需要知道您用于服务托管的框架。
B - 如果您公开远程接口,它将限制功能到可以访问 tcp 端口的客户端(在 domian/network 内)
我对实施版本控制的建议是为您的服务公开定义明确的 Web Api 'JSON/REST' 端点。您可以轻松区分 V1 和 V2,同时您将能够使用单个 'Customer' class。
- 版本 V1 将仅公开带有 Id 和 Name 的方法
- 版本 V2 将公开该方法以及任何新添加的数据成员。
如果出于某种原因您仍然希望将远程处理与版本控制一起用于您的服务。您应该查看 在独占进程中部署您的服务,它允许部署同一服务的多个版本。此部署模型可用于两个场景
- You want to maintain multiple versions of applications
- You want to support isolated applications for multiple client in a single cluster (to save cost).
在以下内容中阅读有关应用程序部署的更多信息 link
https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-hosting-model
https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-concepts-scalability
我有一个服务结构有状态服务,它公开了一个接口,如:
public interface IAction
{
Task GetCustomer (Customer customer)
}
客户 class 看起来像
[DataContract]
public class Customer
{
[DataMember]
public string Id {get;set;}
[DataMember]
public string Name {get;set;}
}
我现在已经通过 nuget 与服务的客户端共享了包含上述模型和接口的程序集。
一段时间后,我需要为其他客户更新客户 class,因此我通过添加额外的可为空 属性
来执行以下操作[DataContract]
public class Customer
{
[DataMember]
public string Id {get;set;}
[DataMember]
public string Name {get;set;}
[DataMember]
public ulong? Salary {get;set;}
}
因为我已经添加了一个可为空的数据成员,所以我假设我只需要与新客户共享这个新模型和合同,而第一个客户不需要更新。
但是,我注意到出现以下异常:
{"Interface id 'xxxxxxxx' is not implemented by object '**'"}
在阅读了多个 SO 答案(
这是一个很大的限制,因为我不应该被迫更新所有客户端。添加的额外可选参数不应强制更新旧客户端,尤其是在服务可以保证完全向后兼容性的情况下。
有没有办法解决以向后兼容的方式更新服务接口而不必更新旧客户端的问题?
这是remoting的一个限制,调用者和发送者必须清楚地理解同一个消息(版本)才能进行通信,如果彼此不理解,就会抛出这个异常。
如果您必须具有这种灵活性,您可以:
This is quite a big limitation, as I should not be forced to update all clients. Extra optional parameters added should not force the old clients to be updated, especially if the service can guarantee full backwards compatibility.
这根本不是限制,因为第一,服务不应该知道客户端,第二,添加 operations/members 不被视为破坏,因为客户端不需要知道添加。
服务中的合同变更被认为是“非破坏性”。您共享的 SO 上其他类似问题的链接并不能完全解决您的问题,可以将其归类为以下要点:
- 服务发现
- 服务版本控制
- 实施 IExtensibleDataObject(如果问题是由 data/data 类型在服务的新添加往返之后引起的)
1 和 2 是关于客户端正确发现服务,使用你的服务的客户端需要知道它 松散的版本控制 兼容性,即 您需要确认客户端在调用您的服务之前没有对旧服务执行任何模式验证。如果是这种情况,那么您需要使用 显式 XML 命名空间 并定义新契约和新服务定义。
这不是限制,但与严格版本控制保持一致,而且它还涉及早期客户端绑定未知的数据类型您的服务因回调而出现异常故障,这是合理的,您应该接受它与 SOA 无关。
要使用此解决方案,您可能需要按以下方式定义合同和服务:
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 {...}
}
和新添加的成员的另一个版本,
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 { ... }
}
关于这段代码的来源,您可以参考这个link,这一定会帮助您了解您的服务有什么问题以及如何修改它。
只是事后将此添加到 DataMember 的 isRequired 属性,默认情况下为 false。
以下引用自here。
If a default value of null or zero for the member is unacceptable, a callback method should be provided using the
OnDeserializingAttribute
to provide a reasonable default in case the member is not present in the incoming stream.
[OnDeserialized]
public void OnDeserialized(StreamingContext context)
{
if (this.id == null) throw new ArgumentNullException("id");
if (this.Name == null) throw new ArgumentOutOfRangeException("name");
if (this.Salary < 0) throw new ArgumentOutOfRangeException("salary");
if (this.Salary > 0)
{
throw new InvalidOperationException("No child labor allowed");
}
}
要支持具有相同远程服务的多个客户端,您需要两个不同的定义明确的 contracts/interfaces。这是根据服务结构中远程处理的设计。
通常,您应该只允许远程处理结构集群内的服务通信。原因是因为
A - DataContractSerializer 与服务结构紧密耦合。而且我假设您的客户不知道或不需要知道您用于服务托管的框架。
B - 如果您公开远程接口,它将限制功能到可以访问 tcp 端口的客户端(在 domian/network 内)
我对实施版本控制的建议是为您的服务公开定义明确的 Web Api 'JSON/REST' 端点。您可以轻松区分 V1 和 V2,同时您将能够使用单个 'Customer' class。
- 版本 V1 将仅公开带有 Id 和 Name 的方法
- 版本 V2 将公开该方法以及任何新添加的数据成员。
如果出于某种原因您仍然希望将远程处理与版本控制一起用于您的服务。您应该查看 在独占进程中部署您的服务,它允许部署同一服务的多个版本。此部署模型可用于两个场景
- You want to maintain multiple versions of applications
- You want to support isolated applications for multiple client in a single cluster (to save cost).
在以下内容中阅读有关应用程序部署的更多信息 link
https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-hosting-model
https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-concepts-scalability