隨著版本的不斷演化,用戶端與服務端的資料契約可能會出現版本不一致的情況。在WCF中,關於資料契約的版本控制有兩種情況:新增成員與缺失成員。新增成員是指發送方包含了新增成員,預設處理方式為忽略新增成員。缺失成員則是指發送方缺少成員,預設處理方式是為缺失成員賦予其預設值。
在缺失成員的情況下,如果僅僅是為缺少的成員賦予預設值,有時候會出現無法預料的錯誤。原因在於缺失的成員有可能是正確執行操作的必要條件。為了避免出現這樣的情況,可以將缺失的成員設定為必備成員,方法是利用DataMember特性的IsRequired屬性,將其值設定為true。例如:
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
[DataMember(IsRequired = true)]
public string Address;
}
如果訊息中的成員被標記為必備成員,當接收端的DataContractSerializer無法找到所需的資訊進行還原序列化時,就會取消這次調用,發送端會引發NetDispatcherFaultException異常。例如,服務端的資料契約如上的定義,其中Address欄位為必備成員,而用戶端的資料契約則如下所示:
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
此時,如果用戶端向服務發出調用,則由於引發了異常,該調用就不會到達服務。
用戶端和服務都能夠將它們的資料契約中的部分或所有資料成員標記為必備,彼此之間是完全獨立的。被標記為必備的成員越多,則與服務或用戶端之間的互動就越安全,但這卻是以犧牲靈活性與版本相容性為代價的。
本書總結了資料契約版本控制的幾種情形,並以表顯示了必備成員的版本相容性:
IsRequired |
V1 to V2 |
V2 to V1 |
False |
Yes |
Yes |
True |
No |
Yes |
假定V2包含了V1的所有資料成員,同時還定義了新增成員。則表涵蓋了版本的幾種情況。
V1到V2:代表了缺失成員的情況。如果IsRequired為false,則互動正常,對於缺失成員則設定為預設值。如果IsRequired為true,就會拋出異常,訊息不能正常發送。
V2到V1:代表了新增成員的情況。不管IsRequired的值為true還是false,WCF均以忽略新成員的方式進行互動,互動正常。
WCF對於一些特殊的資料類型,支援仍然不夠。這在一定程度上限制了CLR開發人員對WCF的設計。這些特殊的資料類型包括:枚舉、委託、DataSet和DataTable、泛型、集合。
枚舉
WCF對枚舉的支援還算不錯。首先,枚舉類型自身是支援序列化的。不需要設定任何特性,枚舉類型的所有成員都會是資料契約的一部分。如果,枚舉類型中只有一部分成員需要成為資料契約的一部分,就需要用到DataContract與EnumMember特性。例如:
[DataContract]
enum ContactType
{
[EnumMember]
Customer,
[EnumMember]
Vendor,
//Will not be part of data contract
Partner
}
用戶端產生的表示形式則為:
enum ContactType
{
Customer,
Vendor
}
委託
WCF對委託以及事件的支援都不夠好。這是因為委託的內部調用列表的具體結構是本地的,用戶端或服務無法跨服務邊界共用委託列表的結構。此外,我們不能保證內部列表中的目標對象都是可序列化的,或者都是有效資料契約。這會導致序列化的操作時而成功,時而失敗。因此,最佳實務是不要將委託成員或事件作為資料契約的一部分。
資料集與資料表
DataSet和DataTable類型是可序列化的,因而我們可以在服務契約中接收或返回資料表或資料集。
如果服務契約使用了DataSet和DataTable類型,產生的代理檔案不會直接使用DataSet和DataTable類型,而是包含DataTable資料契約的定義(只包含DataTable的樣式,而不包含任何代碼)。但我們可以手工修改這些定義。例如這樣的服務契約:
[ServiceContract()]
public interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
void AddContacts(DataTable contacts);
[OperationContract]
DataTable GetContacts();
}
那麼產生的代理檔案可能會是這樣:
public interface IContactManager
{
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/AddContact", ReplyAction="http://tempuri.org/IContactManager/AddContactResponse")]
[System.ServiceModel.XmlSerializerFormatAttribute()]
void AddContact(Contact contact);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/AddContacts", ReplyAction="http://tempuri.org/IContactManager/AddContactsResponse")]
[System.ServiceModel.XmlSerializerFormatAttribute()]
AddContactsResponse AddContacts(AddContactsRequest request);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IContactManager/GetContacts", ReplyAction="http://tempuri.org/IContactManager/GetContactsResponse")]
[System.ServiceModel.XmlSerializerFormatAttribute()]
GetContactsResponse GetContacts(GetContactsRequest request);
}
代理類的定義則如下所示:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class ContactManagerClient : System.ServiceModel.ClientBase<IContactManager>, IContactManager
{
//其餘成員略;
public void AddContact(Contact contact)
{
base.Channel.AddContact(contact);
}
AddContactsResponse IContactManager.AddContacts(AddContactsRequest request)
{
return base.Channel.AddContacts(request);
}
public void AddContacts(AddContactsContacts contacts)
{
AddContactsRequest inValue = new AddContactsRequest();
inValue.contacts = contacts;
AddContactsResponse retVal = ((IContactManager)(this)).AddContacts(inValue);
}
GetContactsResponse IContactManager.GetContacts(GetContactsRequest request)
{
return base.Channel.GetContacts(request);
}
public GetContactsResponseGetContactsResult GetContacts()
{
GetContactsRequest inValue = new GetContactsRequest();
GetContactsResponse retVal = ((IContactManager)(this)).GetContacts(inValue);
return retVal.GetContactsResult;
}
}
我們可以手動將AddContacts()與GetContacts()方法修改為:
public void AddContacts(DataTable contacts)
{
AddContactsRequest inValue = new AddContactsRequest();
inValue.contacts = contacts;
AddContactsResponse retVal = ((IContactManager)(this)).AddContacts(inValue);
}
public DataTable GetContacts()
{
GetContactsRequest inValue = new GetContactsRequest();
GetContactsResponse retVal = ((IContactManager)(this)).GetContacts(inValue);
return retVal.GetContactsResult;
}
當然,前提條件是我們需要修改AddContactRequest類以及GetContactsResponse,例如將AddContactRequest類的contacts成員由原來的AddContactsContacts類型修改為DataTable類型;將GetContactsResponse中的GetContactsResult成員由原來的GetContactsResponseGetContactsResult類型修改為DataTable類型。
自動產生的代理類非常複雜,實際上我們完全可以簡化。首先將用戶端的服務契約定義修改為與服務端服務契約完全一致的定義:
[ServiceContract()]
public interface IContactManager
{
[OperationContract]
void AddContact(Contact contact);
[OperationContract]
void AddContacts(DataTable contacts);
[OperationContract]
DataTable GetContacts();
}
然後修改代理類ContactManagerClient:
public partial class ContactManagerClient : System.ServiceModel.ClientBase<IContactManager>, IContactManager
{
public void AddContact(Contact contact)
{
base.Channel.AddContact(contact);
}
public void AddContacts(DataTable contacts)
{
base.Channel.AddContacts(contacts);
}
public DataTable GetContacts()
{
return base.Channel.GetContacts();
}
}
修改後啟動並執行結果完全相同。
注意,DataRow類型是不能序列化的。
在WCF中,還可以使用DataTable和DataSet的型別安全的子類。書中也給出了相應的例子。然而,WCF的最佳實務則是避免使用DataTable和DataSet,以及使用DataTable和DataSet的型別安全的子類。書中闡釋了原因:
“對於WCF的用戶端與服務而言,雖然可以通過ADO.NET和Visual Studio工具使用DataSet、DataTable以及它們的型別安全的派生對象,但這種方式過於繁瑣。而且,這些資料訪問類型都是特定的.NET類型。在序列化時,它們產生的資料契約樣式過於複雜,很難與其它平台進行互動。在服務契約中使用資料表或者資料集還存在一個缺陷,那就是它可能暴露內部的資料結構。同時,將來對資料庫樣式的修改會影響到用戶端。雖然在應用程式內部可以傳遞資料表,但如果是跨越應用程式或公有的服務邊界發送資料表,卻並非一個好的主意。通常情況下,更好的做法是暴露資料的操作而非資料本身。”
最好的做法是將DataTable轉換為數群組類型。書中提供了DataTableHelper類,可以協助將DataTable轉換為數群組類型。
泛型
非常遺憾,我們並不能在資料契約中定義泛型。但是,WCF使用了一個折中的辦法,使得我們可以在服務端照常使用泛型,但在產生的資料契約定義時,泛型會被具體的類型所取代,重新命名的格式為:
<原有名>Of<型別參數名><雜湊值>
WCF還支援將自訂類型作為泛型參數。此外,還可以通過資料契約的Name屬性為匯出的資料契約指定不同的名字。例如,如下的服務端資料契約:
[DataContract]
class SomeClass
{...}
[DataContract(Name = "MyClass")]
class MyClass<T>
{...}
[OperationContract]
void MyMethod(MyClass<SomeClass> obj);
匯出的資料契約為:
[DataContract]
class SomeClass
{...}
[DataContract]
class MyClass
{...}
[OperationContract]
void MyMethod(MyClass obj);
集合
WCF支援泛型集合、定製集合,但與傳統的.NET編程不一樣,WCF對集合的操作存在許多約束。對於這些約束,本書描述得非常清楚。本文不再贅述。