IExtensibleDataObject is not only for backward compatibility

WCF guidelines recommend enhancing all data contracts with support of IExtensibleDataObject interface. If you search the Web for pages that mention “IExtensibleDataObject” you won’t find many – I’ve found just a couple of thousands at the time of writing. And if you narrow the search by adding word “versioning” to it you will discover that majority of the pages that refer to IExtensibleDataObject also talks about versioning.

There is a good reason for this: if data contracts evolve different service versions can still exchange their data assuming contracts are compatible. Moreover, use of IExtensibleDataObject enables new fields to be passed through the old services without loss of data that is not recognized by older components. This can be illustrated with a simple example.

Let’s say we have a service CustomerManager that is responsible for customer management and retrieval. It contains one service and one data contract.

CustomerManager

This is a latest implementation. However, some of the application installations still use a previous version that does not support property NewsletterSubscriber.

OldCustomerManager

 

As long as Customer class is implemented as WCF DataContract, not just a CLR object, clients can access both old and new service versions. And if Customer data contract implements IExtensibleDataObject interface, clients can even implement so called versioning round-trip, when the same Customer instance is sent between different CustomerManager instances without loss of fields only supported in one of the versions. Here’s a test that demonstrates how it works:

[Test]
public void GetAndUpdateCustomer_NewAndOldManager()
{
    string endpointAddress1 = "net.pipe://localhost/CustomerManager";
    m_Host1 = CreateServiceHost(m_Binding, endpointAddress1);

    m_CustomerManagerFactory = CreateChannelFactory(m_Binding, endpointAddress1);
    var customerManager1 = m_CustomerManagerFactory.CreateChannel();

    string endpointAddress2 = "net.pipe://localhost/OldCustomerManager";
    m_Host2 = CreateServiceHost(m_Binding, endpointAddress2);

    m_OldCustomerManagerFactory = CreateChannelFactory(m_Binding, endpointAddress2);
    var customerManager2 = m_OldCustomerManagerFactory.CreateChannel();

    var customer = customerManager1.GetCustomer(1);
    Assert.AreEqual("John", customer.FirstName);
    Assert.AreEqual("Smith", customer.LastName);
    Assert.IsTrue(customer.NewsletterSubscriber);
    Assert.AreEqual(0, customer.UpdateCount);

    customer = customerManager1.UpdateCustomer(customer);
    Assert.IsTrue(customer.NewsletterSubscriber);
    Assert.AreEqual(1, customer.UpdateCount);

    customer = customerManager2.UpdateCustomer(customer);
    Assert.IsTrue(customer.NewsletterSubscriber);
    Assert.AreEqual(2, customer.UpdateCount);
}

In this and other examples m_Binding refers to a named pipes binding used for in-proc instantiation, and CreateServiceHost and CreateChannelFactory are simple utility methods to create WCF host and channel respectively.

As you can see, we send an instance of Customer through new and old CustomerManager, and its NewsletterSubscriber property is set to “true” after each call although this property is unknown to OldCustomerManager isntance. Here’s the latest version of the Customer data contract:

[DataContract(Namespace="MyCompany")] 
public class Customer : IExtensibleDataObject 
{ 
    [DataMember(Order = 1)] 
    public int CustomerId { get; set; } 
 
    [DataMember(Order = 1)] 
    public string FirstName { get; set; } 
 
    [DataMember(Order = 1)] 
    public string LastName { get; set; } 
 
    [DataMember(Order = 2)] 
    public string PaymentMethod { get; set; } 
 
    [DataMember(Order = 2)] 
    public string BillingAddress { get; set; } 
 
    [DataMember(Order = 2)] 
    public string DeliveryAddress { get; set; } 
 
    [DataMember(Order = 3)] 
    public bool NewsletterSubscriber { get; set; } 
 
    [DataMember(Order = 100)] 
    public int UpdateCount { get; set; } 
 
    #region IExtensibleDataObject Members 
 
    public ExtensionDataObject ExtensionData { get; set; } 
 
    #endregion 
}

If you remove support for IExtensibleDataObject, the test will fail: OldCustomerManager will reset unknown properties to default values, and NewsletterSubscriber will be assigned to “false” after a round-trip to this service. Note that compatibility of data contracts requires match of both class names and namespaces, and successful match of fields require match of their names and ordinals, so although Order is an optional attribute, it is recommended to assign its value explicitly.

Enough with versioning, this is not what I was going to focus on. Let’s take a look at some other aspects of service communiation: data hierarchies, data encapsulation and data transfer objects.

Let’s extend a set of our services with a couple of others: OrderManager and ShipmentManager. Each service needs a definition of Customer, but they use only few Customer fields.

OrderManager

ShipmentManager

Note the difference in use of address fields by these services: OrderManager only needs customer’s billing address to make a shipment while ShipmentManager has nothing to do with payment details and requires customer’s delivery address to fulfill the shipment.

How sholud we design such services? One way is to make a common Customer definition that would contain all customer details that might be interesting for any service and pass it around. It’s easy but it ruins the concept of data encapsulation. And it’s easy to imagine even from this examples what can be the consequences of lack of data encapsulation: if all address fields are visible for all services, it’s a great danger that some developer will pick up wrong address when writing the service code.

Another approach would be to equip each service with its own definition of Customer data contract and write adapters to transfer data between services. In general I like the idea of defining everything that is specific for a service in-place. The question is how much of code and maintenance overhead data adapters would add? Especially if number of services is big.

WCF provides its own alternative to support class hierarchies on DataContract level, using KnownType and ServiceKnownType. The biggest downside of known types is that use of them requires knowing in advance all possible subclasses of a given data contract. And examples above show that the main challenge is not in enabling support for class hierachies: data encapsulation across multiple services can easily become a task of configuring visibility of individual fields.

However, if we look back into IExtensibleDataObject usage scenarios, we can find their a possible resolution of this task: make sure your DataContract classes implement IExtensibleDataObject interface and define for each service only the fields it has use for. Let’s have a look at a couple of examples.

[Test] 
public void GetCustomerAndRegisterOrder() 
{ 
    string endpointAddress1 = "net.pipe://localhost/CustomerManager"; 
    m_Host1 = CreateServiceHost(m_Binding, endpointAddress1); 
 
    m_CustomerManagerFactory = CreateChannelFactory(m_Binding, endpointAddress1); 
    var customerManager = m_CustomerManagerFactory.CreateChannel(); 
 
    string endpointAddress2 = "net.pipe://localhost/OrderManager"; 
    m_Host2 = CreateServiceHost(m_Binding, endpointAddress2); 
 
    m_OrderManagerFactory = CreateChannelFactory(m_Binding, endpointAddress2); 
    var orderManager = m_OrderManagerFactory.CreateChannel(); 
 
    var customer = customerManager.GetCustomer(1); 
    Order order = new Order(); 
    order.OrderDetails = "My order details"; 
    int orderId = orderManager.RegisterOrder(customer, order); 
 
    Assert.AreEqual(100, orderId); 
}

Here’s the test output:

Retrieving customer #1

Registering order from customer #1

1 passed, 0 failed, 0 skipped, took 5,58 seconds (NUnit 2.4).

The OrderManager object receives a Customer data from CustomerManager that passes full customer definition. However, inside the OrderManager code only CustomerId, PaymentMethod and BillingAddress are available. It is not entitled to access any other customer data.

And here’s another test:

[Test] 
public void GetCustomerThenRegisterGetAndShipOrder() 
{ 
    string endpointAddress1 = "net.pipe://localhost/CustomerManager"; 
    m_Host1 = CreateServiceHost(m_Binding, endpointAddress1); 
 
    m_CustomerManagerFactory = CreateChannelFactory(m_Binding, endpointAddress1); 
    var customerManager = m_CustomerManagerFactory.CreateChannel(); 
 
    string endpointAddress2 = "net.pipe://localhost/OrderManager"; 
    m_Host2 = CreateServiceHost(m_Binding, endpointAddress2); 
 
    m_OrderManagerFactory = CreateChannelFactory(m_Binding, endpointAddress2); 
    var orderManager = m_OrderManagerFactory.CreateChannel(); 
 
    var customer = customerManager.GetCustomer(1); 
    Order order = new Order(); 
    order.OrderDetails = "My order details"; 
    int orderId = orderManager.RegisterOrder(customer, order); 
    order = orderManager.GetOrder(orderId); 
    orderManager.ShipOrder(customer, order); 
}

Retrieving customer #1

Registering order from customer #1

Retrieving order #100

Shipping order #100 to customer #1, billing to Diamond St. 1 12345 Georgetown

Shipping order #100 to Club Ave. 2 67890 Johnstone

1 passed, 0 failed, 0 skipped, took 5,58 seconds (NUnit 2.4).

The first “Shipping order…” line in the test output comes from the OrderManager that only has a definition of BillingAddress. The second “Shipping order…” line is written by the ShipmentManager that only deals with DeliveryAddress. OrderManager.ShipOrder has the following implementation:

public void ShipOrder(Customer customer, Order order) 
{ 
    Console.WriteLine(string.Format("Shipping order #{0} to customer #{1}, billing to {2}", order.OrderId, customer.CustomerId, customer.BillingAddress)); 
 
    NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None); 
    string endpointAddress = "net.pipe://localhost/ShipmentManager"; 
 
    var host = new ServiceHost(typeof(ShipmentManager.ShipmentManager)); 
    host.AddServiceEndpoint(typeof(ShipmentManager.IShipmentManager), binding, endpointAddress); 
    host.Open(); 
 
    ChannelFactory channel = new ChannelFactory(binding, endpointAddress); 
    var shipmentManager = channel.CreateChannel(); 
 
    shipmentManager.ShipOrder(customer, order); 
 
    channel.Close(); 
    host.Close(); 
}

The implementation of ShipOrder inside ShipmentManager can’t be simpler:

public void ShipOrder(Customer customer, Order order) 
{ 
    Console.WriteLine(string.Format("Shipping order #{0} to {1}", order.OrderId, customer.DeliveryAddress)); 
}

What is interesting here is that ShipmentManager.ShipOrder is called by the OrderManage that does not have access to DeliveryAddress data on property level (it can of course dig it out from ExtensionData but I don’t think such code should pass a code review). Still ShipmentManager receives all information it needs assuming that that it was populated in first place by CustomerManager.

Use of IExtensibleDataObject definitely can become handy in some circumstances, and not only to solve versioning compatibility issues. Should the visibility of data contract members be solved using this technique? I don’t know. Perhaps if data contacts for different services are maintained by different teams or there are great chances that data contracts that once had the same nature will evolve into unrelated schemas, a different approach will serve better. But for related services – especially when their data contracts are managed wtihin a small team – this approach can keep keep services isolated and bring sufficient data encapsulation without use of data converters that usually require much bigger code writing effort.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s