Search code examples
client-serverazure-service-fabricbackwards-compatibility

Backwards compatible service interfaces without updating older clients in azure service fabric


I have a service fabric stateful service which exposes an interface like :

public interface IAction 
{
   Task GetCustomer (Customer customer)
} 

The Customer class looks like

[DataContract]
public class Customer
{
   [DataMember]
   public string Id {get;set;}

   [DataMember]
   public string Name {get;set;}
}

I have now shared the assembly containing the above model and interface with the client of the service via nuget.

After a while I have a need to update the Customer class for other clients, so I do the following, by adding the extra nullable property

[DataContract]
public class Customer
{
   [DataMember]
   public string Id {get;set;}

   [DataMember]
   public string Name {get;set;}

   [DataMember]
   public ulong? Salary {get;set;}
}

Since I have added a nullable data member, I would assume that I need only share this new model and contract with the newer clients and the first client need not bother updating.

However, I notice that I get the following exception :

{"Interface id 'xxxxxxxx' is not implemented by object '**'"}

After having read multiple SO answers(here,here), I am led to the conclusion that always a client must have the exact reference of the interface and model present in the current running version of the service.

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.

Is there a way around this problem of updating the service interface in a backwards compatible way without having to update the older clients ?


Solution

  • 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.

    It is not a limitation at all because one, the service is not supposed to know about the client and two, the addition of operations/members are not considered breaking because the client need not know about the addition.

    Contract changes in a service is considered "non breaking". The links to other similar problem on SO that you have shared are not exactly addressing your problem, which can be categorized into the following bullet points:

    1. Service Discovery
    2. Service Versioning
    3. Implement IExtensibleDataObject (if the problem is caused by the data/data type after a roundtrip with the new addition to the service)

    1 & 2 are about the client discovering the service correctly, the client consuming your service needs to be aware of it for lax versioning compatibility ie., you need to confirm that the client is not performing any schema validation against the old service before even making a call to your service. If this be the case, then you need to use explicit XML namespaces and define new contracts and new service definitions.

    This is not a limitation but keeping in with strict versioning plus it is also about the data types unknown to the earlier client binding with your service that faults with an exception due to a callback, which is reasonable and you should accept it has nothing to do with the SOA.

    To use this solution, you may need to define your contract and service in the following way:

    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 {...}  
    }  
    

    and another version for the newly added member like so,

    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 { ... }  
    }  
    

    For the source of this code, you may refer to this link which will definitely help you understand what is wrong with your service and how to modify it.

    Just added this as an afterthought to isRequired attribute to a DataMember, which is false by default.

    The below is referenced from 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");
        }
    }