六、定義DataContract
我在介紹如何定義一個ServiceContract時,舉了這樣的一個例子,代碼如下:
[ServiceContract]
public class BookTicket
{
[OperationContract]
public bool Check(Ticket ticket)
{
bool flag;
//logic to check whether the ticket is none;
return flag;
}
[OperationContract]
private bool Book(Ticket ticket)
{
//logic to book the ticket
}
}
在Service類BookTicket中,兩個服務方法Check和Book的參數均為Ticket類型。這個類型是自訂類型,根據WCF的要求,該類型必須支援序列化的操作,方才可以在服務方法中作為訊息被傳遞。
在.Net中,除了基本類型如int,long,double,以及枚舉類型和String類型外,一個自訂的類型如要支援序列化操作,應該標記該類型為[Serializable],或者使該類型實現ISerializable介面。而在WCF中,推薦的一種方式是為這些類型標記DataContractAttribute。方法如下:
[DataContract]
public class Ticket
{
private string m_movieName;
[DataMember]
public int SeatNo;
[DataMember]
public string MovieName
{
get {return m_movieName;}
set {m_movieName = value;}
}
[DataMember]
private DateTime Time;
}
其中,[DataMember]是針對DataContract類的成員所標記的Attribute。與服務類中的OperationContractAttribute相同,DataMemberAttribute與對象的訪問限制修飾符(public,internal,private等)沒有直接關係。即使該成員為private,只要標記了[DataMember],仍然可以被序列化。雖然DataMemberAttribute可以被施加給類型的欄位和屬性,但如果被施加到static成員時,WCF會忽略該DataMemberAttribute。
當我們為一個類型標註DataContractAttribute時,只有被顯式標註了DataMemberAttribute的成員方才支援序列化操作。這一點與SerializableAttribute大相徑庭。一個被標記了SerializableAttribute的類型,在預設情況下,其內部的成員,不管是public還是private都支援序列化,除非是那些被施加了NonSerializedAttribute的成員。DataContractAttribute採用這種顯式標註法,使得我們更加專註於服務訊息的定義,只有需要被傳遞的服務訊息成員,方才被標註DataMemberAttribute。
如果DataContract類中的DataMember成員包含了泛型,那麼泛型型別參數必須支援序列化,如下代碼所示:
[DataContract]
public class MyGeneric
{
[DataMember]
T theData;
}
在類MyGeneric中,泛型參數T必須支援序列化。如執行個體化該對象:
MyGeneric intObject = new MyGeneric();
MyGeneric customObject = new MyGeneric();
對象intObject由於傳入的泛型參數為int基本類型,因此可以被序列化;而對象customObject是否能被序列化,則要看傳入的泛型參數CustomType類型是否支援序列化。
DataContract以Namespace和Name來唯一標識,我們可以在DataContractAttribute的Namespace屬性、Name屬性中進行設定。如未設定DataContract的Name屬性,則預設的名字為定義的類型名。DataMember也可以通過設定Name屬性,預設的名字為定義的成員名。如下代碼所示:
namespace MyCompany.OrderProc
{
[DataContract]
public class PurchaseOrder
{
// DataMember名字為預設的Amount;
[DataMember]
public double Amount;
// Name屬性將重寫預設的名字Ship_to,此時DataMember名為Address;
[DataMember(Name = “Address”)]
public string Ship_to;
}
//Namespace為預設值:
// http://schemas.datacontract.org/2004/07/MyCompany.OrderProc
//此時其名為PurchaseOrder而非MyInvoice
[DataContract(Name = “PurchaseOrder”)]
public class MyInvoice
{
// Code not shown.
}
// 其名為Payment而非MyPayment
// Namespace被設定為http://schemas.example.com
[DataContract(Name = “Payment”,
Namespace = “http://schemas.example.com“)]
public class MyPayment
{
// Code not shown.
}
}
// 重寫MyCorp.CRM下的所有DataContract的Namespace
[assembly:ContractNamespace(
ClrNamespace = “MyCorp.CRM”,
Namespace= “http://schemas.example.com/crm“)]
namespace MyCorp.CRM
{
// 此時Namespace被設定為http://schemas.example.com/crm.
// 名字仍然為預設值Customer
[DataContract]
public class Customer
{
// Code not shown.
}
}
由於DataContract將被序列化以及還原序列化,因此類型中成員的順序也相當重要,在DataMemberAttribute中,提供了Order屬性,用以設定成員的順序。在WCF中對成員的序列化順序規定如下:
1、預設的順序依照字母順序;
2、如成員均通過Order屬性指定了順序,且順序值相同,則以字母順序;
3、未指定Order屬性的成員順序在指定了Order順序之前;
4、如果DataContract處於繼承體系中,則不管子類中指定的Order值如何,父類的成員順序優先。
下面的代碼很好的說明了DataMember的順序:
[DataContract]
public class BaseType
{
[DataMember] public string zebra;
}
[DataContract]
public class DerivedType : BaseType
{
[DataMember(Order = 0)] public string bird;
[DataMember(Order = 1)] public string parrot;
[DataMember] public string dog;
[DataMember(Order = 3)] public string antelope;
[DataMember] public string cat;
[DataMember(Order = 1)] public string albatross;
}
序列化後的XML內容如下:
<DerivedType>
<zebra/>
<cat/>
<dog/>
<bird/>
<albatross/>
<parrot/>
<antelope/>
</DerivedType>
因為成員zebra為父類成員,首先其順序在最前面。cat和dog未指定Order,故在指定了Order的其它成員之前,兩者又按照字母順序排列。parrot和albatross均指定Order值為1,因此也按照字母順序排列在Order值為0的bird之後。
判斷兩個DataContract是否相同,應該根據DataContract的Namespace和Name,以及DataMember的Name和Order來綜合判斷。例如下面代碼所示的類Customer和Person其實是同一個DataContract:
[DataContract]
public class Customer
{
[DataMember]
public string fullName;
[DataMember]
public string telephoneNumber;
}
[DataContract(Name=”Customer”)]
public class Person
{
[DataMember(Name = “fullName”)]
private string nameOfPerson;
private string address;
[DataMember(Name= “telephoneNumber”)]
private string phoneNumber;
}
再例如下面代碼所示的類Coords1、Coords2、Coords3也是相同的DataContract,而類Coords4則因為順序不同,因而與前面三個類是不同的:
[DataContract(Name= “Coordinates”)]
public class Coords1
{
[DataMember] public int X;
[DataMember] public int Y;
}
[DataContract(Name= “Coordinates”)]
public class Coords2
{
[DataMember] public int Y;
[DataMember] public int X;
}
[DataContract(Name= “Coordinates”)]
public class Coords3
{
[DataMember(Order=2)] public int Y;
[DataMember(Order=1)] public int X;
}
[DataContract(Name= “Coordinates”)]
public class Coords4
{
[DataMember(Order=1)] public int Y;
[DataMember(Order=2)] public int X;
}
當DataContract處於繼承體系時,還需要注意的是對象的“多態”問題。如果在服務端與用戶端之間要傳遞訊息,經常會涉及到類型的動態綁定。根據規定,如果訊息的類型是子類類型,那麼發送訊息一方就不能傳遞基類類型。相反,如果訊息類型是父類類型,那麼發送訊息一方就可以是父類本身或者其子類。從這一點來看,WCF的規定是與物件導向思想並行不悖的。但是可能存在的問題是,當訊息類型定義為父類類型,而發送訊息一方傳遞其子類時,服務端有可能對該子類類型處於“未知”狀態,從而不能正常地還原序列化。所以,WCF為DataContract提供了KnownTypeAttribute,通過設定它來告知服務端可能存在的動態綁定類類型。
舉例來說,如果我們定義了這樣三個類:
[DataContract]
public class Shape { }
[DataContract(Name = “Circle”)]
public class CircleType : Shape { }
[DataContract(Name = “Triangle”)]
public class TriangleType : Shape { }
然後在類CompanyLogo中定義Shape類型的欄位,如下所示:
[DataContract]
public class CompanyLogo
{
[DataMember]
private Shape ShapeOfLogo;
[DataMember]
private int ColorOfLogo;
}
此時的CompanyLogo類由於正確的設定了[DataContract]和[DataMember],而Shape類型也是支援序列化的,因此該類型是可以被序列化的。然而一旦用戶端在調用CompanyLogo類型的對象時,為欄位ShapeOfLogo設定其值為CircleType或TriangleType類型的對象,就會發生還原序列化錯誤,因為服務端並不知道CircleType或TriangleType類型,從而無法進行正確的匹配。所以上述的CompanyLogo類的定義應修改如下:
[DataContract]
[KnownType(typeof(CircleType))]
[KnownType(typeof(TriangleType))]
public class CompanyLogo
{
[DataMember]
private Shape ShapeOfLogo;
[DataMember]
private int ColorOfLogo;
}
類的繼承如此,介面的實現也是同樣的道理,如下例所示:
public interface ICustomerInfo
{
string ReturnCustomerName();
}
[DataContract(Name = “Customer”)]
public class CustomerType : ICustomerInfo
{
public string ReturnCustomerName()
{
return “no name”;
}
}
[DataContract]
[KnownType(typeof(CustomerType))]
public class PurchaseOrder
{
[DataMember]
ICustomerInfo buyer;
[DataMember]
int amount;
}
由於PurchaseOrder中定義了ICustomerInfo介面類型的欄位,如要該類能被正確的還原序列化,就必須為類PurchaseOrder加上[KnownType(typeof(CustomerType))]的標註。
對於集合類型也有相似的規定。例如Hashtable類型,其記憶體儲的均為object對象,但實際設定的值可能是一些自訂類型,此時也許要通過KnownType進行標註。例如在類LibraryCatalog中,定義了Hashtable類型的欄位theCatalog。該欄位可能會設定為Book類型和Magazine類型,假定Book類型和Magazine類型均被定義為DataContract,則類LibraryCatalog的正確定義應如下所示:
[DataContract]
[KnownType(typeof(Book))]
[KnownType(typeof(Magazine))]
public class LibraryCatalog
{
[DataMember]
System.Collections.Hashtable theCatalog;
}
如果在一個DataContract中,定義一個object類型的欄位。由於object類型是所有類型的父類,所以需要我們利用KnownType標明用戶端允許設定的類型。例如類MathOperationData:
[DataContract]
[KnownType(typeof(int[]))]
public class MathOperationData
{
private object numberValue;
[DataMember]
public object Numbers
{
get { return numberValue; }
set { numberValue = value; }
}
//[DataMember]
//public Operation Operation;
}
屬性Numbers其類型為object,而KnownType設定的類型是int[],因此可以接受的類型就包括:整型,整型數組以及List類型。如下的調用都是正確的:
static void Run()
{
MathOperationData md = new MathOperationData();
int a = 100;
md.Numbers = a;
int[] b = new int[100];
md.Numbers = b;
List c = new List();
md.Numbers = c;
}
但如果設定Number屬性為ArrayList,即使該ArrayList對象中元素均為int對象,也是錯誤的:
static void Run()
{
MathOperationData md = new MathOperationData();
ArrayList d = new ArrayList();
md.Numbers = d;
}
一旦一個DataContract類型標註了KnownTypeAttribute,則該Attribute的範圍可以施加到其子類中,如下所示:
[DataContract]
[KnownType(typeof(CircleType))]
[KnownType(typeof(TriangleType))]
public class MyDrawing
{
[DataMember]
private object Shape;
[DataMember]
private int Color;
}
[DataContract]
public class DoubleDrawing : MyDrawing
{
[DataMember]
private object additionalShape;
}
雖然DoubleDrawing沒有標註KnowTypeAttribute,但其欄位additionalShape仍然可以被設定為CircleType類型或TriangleType類型,因為其父類已經被設定為KnowTypeAttribute。
註:KnowTypeAttribute可以標註類和結構,但不能標註介面。此外,DataContract同樣不能標註介面,僅可以標註類、結構和枚舉。要使用DataContractAttribute、DataMemberAttribute和KnownTypeAttribute,需要添加WinFx版本的System.Runtime.Serialization程式集的引用。