軟件工程是一門獨特的工程藝術,需要解決的是不斷改變的需求變化。而對於WCF,對於SOA,由於涉及的是對多個系統之間的交互問題,如何有效地解決不斷改變的需求所帶來的問題就顯得更為重要:Service端版本的變化能否保持現有Consumer的正常調用,Consumer端的改變不至於影響對Service 的正常調用。對於Data Contract來說就是要解決這樣的問題:Service端或者Client對Data Type的改變不會影響Service的正常調用。
在系統開發過程中,通過對Data Type添加額外的字段進而對其進行擴展,是一個種很常見的場景。本部分就作中介紹Data Contract的這種變化,Service或者Client的Data Contract在本地添加一個新的Data Member會造成怎樣的影響,WCF可以采用怎樣的機制來解決這種單方面Data Contract版本的改變。
我們同樣通過Dome來說話。在這個Demo中,我使用上面介紹的Order Processing的場景,下面是整個Solution的結構(需要說明的是,本片文章提供的Code片斷和Source Code都是基於VS 2008的)。
1.Service端: Artech.DataContractVersioning.Service
Data Contract
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;
namespace Artech.DataContractVersioning.Service
{
[DataContract(Namespace="http://artech.datacontractversioning")]
public class Order
{
[DataMember(Order = 0)]
public Guid OrderID
{get;set;}
[DataMember(Order = 1)]
public DateTime OrderDate
{ get; set; }
[DataMember(Order = 2)]
public Guid SupplierID
{ get; set; }
}
}
Service Contract 和Service Implementation: Process方法簡單地將Order對象返回到客戶端,當Client接受到Service返回的Order對象後,可以檢測和由它傳遞給Service的Order對象有什麼不同。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace Artech.DataContractVersioning.Service
{
[ServiceContract]
public interface IOrderManager
{
[OperationContract]
Order Process(Order order);
}
}
namespace Artech.DataContractVersioning.Service
{
public class OrderManagerService:IOrderManager
{
IOrderManager Members#region IOrderManager Members
public Order Process(Order order)
{
return order;
}
#endregion
}
}
2.Client端:
Data Contract
[DataContract(Name="Order",Namespace="http://artech.datacontractversioning")]
public class CustomOrder
{
[DataMember(Order = 0, Name="OrderID")]
public Guid OrderNo
{ get; set; }
[DataMember(Order = 2, Name = "SupplierID")]
public Guid SupplierNo
{ get; set; }
[DataMember(Order = 1)]
public DateTime OrderDate
{ get; set; }
}
}
Program:先創建一個Order對象,向Console打印出Order的信息,隨後以此作為參數調用Service,最後將返回的Order對象的信息打印出來,看看兩者之間的有何區別。
namespace Artech.DataContractVersioning.Client
{
class Program
{
static void Main(string[] args)
{
ChannelFactory<IOrderManager> channelFactory = new ChannelFactory<IOrderManager>("orderManager.http");
IOrderManager orderManager = channelFactory.CreateChannel();
try
{
CustomOrder order = new CustomOrder { OrderNo = Guid.NewGuid(), SupplierNo = Guid.NewGuid(), OrderDate = DateTime.Today, ShippingAddress="Room E101, Airport Rd #328, Suzhou Jiangsu Province" };
Console.WriteLine("The original order: \n{0}", order.ToString());
order = orderManager.Process(order);
Console.WriteLine("\n\nThe order processed by service: \n{0}", order.ToString());
}
finally
{
(orderManager as IDisposable).Dispose();
}
Console.Read();
}
}
}
通過上面的分析,我們可以知道,盡管就CLR Type的定義來講,Service端的Order和Client端的CustomOrder具有很大的差異,但是通過WCF Datacontract Attribute的適配,他們是相互匹配的。
現在我們在Client端為Custom添加一個新的成員,ShippingAddress,通過重寫ToString方法:
namespace Artech.DataContractVersioning.Client
{
[DataContract(Name="Order",Namespace="http://artech.datacontractversioning")]
public class CustomOrder
{
[DataMember(Order = 0, Name="OrderID")]
public Guid OrderNo
{ get; set; }
[DataMember(Order = 2, Name = "SupplierID")]
public Guid SupplierNo
{ get; set; }
[DataMember(Order = 1)]
public DateTime OrderDate
{ get; set; }
[DataMember(Order = 3)]
public string ShippingAddress
{ get; set; }
public override string ToString()
{
return string.Format("Order No.\t: {0}\nSupplier No.\t: {1}\nOrder Date:\t: {2}\nShipping Address: {3}", this.OrderNo, this.SupplierNo, this.OrderDate, this.ShippingAddress);
}
}
}
我們來看看Client端程序運行的輸出結果:
通過上面的結果,我們發現Shipping Address的信息在經過Service處理後丟失了。原因很簡單,Service端的Data Contract根本就沒有ShippingAddress成員,所有在反序列化生成Order對象的時候將會忽略ShippingAddress的信息。
其實這是一個不太合理的狀況,對於Client來說,我指定了對象的某個對象的某個成員的值,結果Service處理返回後,卻無緣無故(對於Client來說是無緣無故)丟失了。其實這種情況還出來在另一種場景之中:Client先調用Service A,Service B再將相同的對象作為參數調用Service C,現在假設Client和Service B的Data Contract是CustomOrder,Service A的Data Contract是少一個ShippingAddress的Order,那麼經過Service A反序列化的對象將會是缺少Shipping Address的Order對象,然後這個Order對象又由Service A傳導Service B,雖然Service B能過識別Shipping Address成員,但是現在卻沒有改成員的值了,這顯然是有問題的。我們把這樣的問題稱為Round trip問題,我們必須解決這樣一個問題。
其實在WCF中解決這樣一個問題的方案簡單而直接,那就是在Data Contract中定義一個額外的成員來存儲沒有在成員列表中定義的信息。我們可以讓Data Contract的Data Type實現System.Runtime.Serialization.IExtensibleDataObject Interface來解決Round trip的版本問題。Interface的定義如下,他僅僅有一個Property成員:ExtensionData。
namespace System.Runtime.Serialization
{
// Summary:
// Provides a data structure to store extra data encountered by the System.Runtime.Serialization.XmlObjectSerializer
// during deserialization of a type marked with the System.Runtime.Serialization.DataContractAttribute
// attribute.
public interface IExtensibleDataObject
{
// Summary:
// Gets or sets the structure that contains extra data.
//
// Returns:
// An System.Runtime.Serialization.ExtensionDataObject that contains data that
// is not recognized as belonging to the data contract.
ExtensionDataObject ExtensionData { get; set; }
}
}
現在我們來重新定義Service的Order Data Contract:
namespace Artech.DataContractVersioning.Service
{
[DataContract(Namespace="http://artech.datacontractversioning")]
public class Order:IExtensibleDataObject
{
[DataMember(Order = 0)]
public Guid OrderID
{get;set;}
[DataMember(Order = 1)]
public DateTime OrderDate
{ get; set; }
[DataMember(Order = 2)]
public Guid SupplierID
{ get; set; }
public ExtensionDataObject ExtensionData
{
get;
set;
}
}
}
我們再來運行一下client端程序,我們發現現在沒有數據丟失了:
這就是實現了IExtensibleDataObject Interface的效果。就其本質,很簡單,對於實現了該Interface的Data contract,將通過一個ExtensionDataObject 類型的對象來保存和獲取那些沒有在Data Contract定義的成員。為了一窺Order的ExtensionData屬性中保存的內容,我們在Service進行Debug,在QuickWatch中看看它是不是真的保存了不能識別的ShippingAddress: