WCF把書讀薄(3)——數據契約、消息契約與錯誤契約

  上一篇:WCF把書讀薄(2)——消息交換、服務實例、會話與併發html

 

  12、數據契約編程

  在實際應用當中數據不可能僅僅是以int Add(int num1, int num2)這種簡單的幾個int的方式進行傳輸的,而是要封裝成相對複雜的Request/Response對象,即用咱們自定義的類來進行消息的傳輸,那麼就須要一種規則來序列化/反序列化咱們本身的對象成爲某種標準格式。WCF能夠經過數據契約來完成這一過程,WCF使用的序列化器是DataContractSerializer。服務器

  在一個類上打上DataContract標記表示這是一個數據契約,其中打上DataMember的屬性會被WCF序列化,與是否public無關(P174),例子:併發

[DataContract]
public class Customer
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public string Phone { get; set; }
    [DataMember]
    public Address CompanyAddress { get; set; }
    [DataMember]
    public Address ShipAddress { get; set; }
}
[DataContract]
public class Address
{
    [DataMember]
    public string Province { get; set; }
    [DataMember]
    public string City { get; set; }
    [DataMember]
    public string District { get; set; }
    [DataMember]
    public string Road { get; set; }
}

  DataContract有三個屬性,其中Name和NameSpace表示名稱和命名空間,IsReference表示若是設置爲true,則在序列化XML的過程中,若是遇到了兩個對象使用同一個對象的引用,則只序列化一份這個對象,默認爲false(P181)。app

  DataMember有四個屬性,Name爲序列化後在XML中的節點名稱,Order爲在XML中的排序,默認爲-1,從小到大排序,在咱們隊序列化後的結果不滿意時能夠經過這個屬性進行修改,序列化後的數據規則是:父類在前之類在後,同一類型中的成員按照字母排序,IsRequired表示屬性成員是不是必須成員,默認爲false可缺省的,EmitDefaultValue表示該值等於默認值時是否序列化,默認爲true。框架

  在應用當中服務可能來回傳遞很大的DataSet,致使服務器端序列化不堪重負,因而能夠修改WCF服務行爲的maxItemInObjectGraph的值來控制最大序列化對象的數量上限,好比設置爲2147483647(P178)。如何設置服務行爲這裏再也不贅述,能夠看個人上一篇筆記。分佈式

  SOAP消息裏的內容是使用DataContractSerializer序列化的,固然,若是想換一種序列化方式,能夠在服務契約類上打標籤好比[XmlSerializerFormat]。ide

  

  十3、繼承關係的序列化ui

  依舊是老A的例子,假設有以下的數據契約和服務:this

public interface IOrder
{
    Guid Id { get; set; }
    DateTime Date { get; set; }
    string Customer { get; set; }
    string ShipAddress { get; set; }
}

[DataContract]
public abstract class OrderBase : IOrder
{
    [DataMember]
    public Guid Id { get; set; }
    [DataMember]
    public DateTime Date { get; set; }
    [DataMember]
    public string Customer { get; set; }
    [DataMember]
    public string ShipAddress { get; set; }
}

[DataContract]
public class Order : OrderBase
{
    [DataMember]
    public double TotalPrice { get; set; }
}

[ServiceContract]
public interface IOrderService
{
    [OperationContract]
    void ProcessOrder(IOrder order);
}

在這裏數據契約存在繼承關係且實現了一個接口,服務契約須要傳入一個接口類型做爲參數,那麼元數據發佈後,在客戶端就會獲得以下的方法:

public void ProcessOrder(object order) {
    base.Channel.ProcessOrder(order);
}

其類型變成了object,這就會形成危險,因此說不推薦在服務操做中使用接口類型做爲參數。通過我的實踐證實,即使用ServiceKnownType屬性,到了客戶端也是一個object類型參數。形成這一現象的緣由就是WCF不知道如何序列化服務契約當中的IOrder,它不知道這表明了什麼,因而序列化到XML時這個數據類型對應的節點就是<anyType>。

  一個恰當的改法就是利用已知類型,修改服務契約,讓他使用父類而不是接口,而且修改數據契約,給父類設置之類的已知類型:

[ServiceContract]
public interface IOrderService
{
    [OperationContract]
    void ProcessOrder(OrderBase order);
}

[DataContract]
[KnownType(typeof(Order))]
public abstract class OrderBase : IOrder
{
    [DataMember]
    public Guid Id { get; set; }
    [DataMember]
    public DateTime Date { get; set; }
    [DataMember]
    public string Customer { get; set; }
    [DataMember]
    public string ShipAddress { get; set; }
}

如此一來,到客戶端參數就成爲了OrderBase類型,正如咱們所願的。

  另外一套解決方案是數據契約不變,把針對已知類型的配置放在操做契約上,一樣操做契約不能使用接口,以下:

[ServiceContract]
[ServiceKnownType("GetKnownTypes", typeof(KnownTypeResolver))]
public interface IOrderService
{
    [OperationContract]
    void ProcessOrder(OrderBase order);
}

public static class KnownTypeResolver
{
    public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider)
    {
        yield return typeof(Order);
    }
}

這裏經過一個類來反射獲取已知類型。

 

  十4、數據契約的版本控制

  不論服務端仍是客戶端,他們的之間發送的數據都是要序列化爲XML的,序列化的依據就是XSD,若是雙方要保持正常通訊,那麼這個XSD就必須等效,這個「等效」指的是契約命名空間和各屬性的名稱及順序都必須一致。

  然而程序並非一成不變的,隨着需求變化,咱們可能會在服務端的數據契約當中增刪一些字段,而沒有更新服務引用的客戶端在和新版本的服務交互時就會發生問題,對於這種版本不一致形成的問題,WCF提供瞭解決方案。

  第一種狀況是服務端增長了一個字段,而客戶端依然經過老版本的數據契約進行服務調用,如此一來在服務端反序列化時就會發現缺乏字段,在這種狀況下,對於缺乏的字段,服務端會自動採用默認值來填充(P210),若是但願客戶端不更新服務則調用錯誤的話,就須要加上表示數據成員是必須傳入的反射標記了:

[DataMember(IsRequired=true)]
public string Description { get; set; }

  若是但願不實用默認值,而實用我麼自定義的值,則須要在數據契約內增長方法:

[DataContract]
public abstract class OrderBase : IOrder
{
    [DataMember]
    public Guid Id { get; set; }
    [DataMember]
    public DateTime Date { get; set; }
    [DataMember]
    public string Customer { get; set; }
    [DataMember]
    public string ShipAddress { get; set; }
    [DataMember]
    public string Description { get; set; }

    [OnDeserializing]
    void OnDeserializing(StreamingContext context)
    {
        this.Description = "NoDescription";
    }
}

  和OnDeserializing相似的還有OnDeserialized、OnSerializing,OnSerialized幾個標籤,能夠在其中增長序列化先後事件。

  第二種狀況是服務端減小了一個字段,在這種狀況下采用新版本數據契約的服務端在會發給採用老版本數據契約的客戶端時就會出現數據丟失的狀況。

  在這種狀況下,須要給數據契約實現IExtensibleDataObject接口,並注入ExtensionDataObject類型的ExtensionData屬性:

[DataContract]
public abstract class OrderBase : IOrder, IExtensibleDataObject
{
    [DataMember]
    public Guid Id { get; set; }
    [DataMember]
    public DateTime Date { get; set; }
    [DataMember]
    public string Customer { get; set; }
    [DataMember]
    public string ShipAddress { get; set; }

    public ExtensionDataObject ExtensionData { get; set; }
}

有了這個屬性,在序列化的時候就會自動帶上額外的屬性了,固然,若是但願屏蔽掉這個功能,則須要在服務行爲和終結點行爲當中進行配置:

<dataContractSerializer ignoreExtensionDataObject="true" />

 

  十5、消息契約

  其實利用數據契約已經可以很好地完成數據的傳輸了,而數據契約只能控制消息體,有時候咱們想在數據傳遞過程當中添加一些額外信息,而不但願添加額外的契約字段,那麼咱們就得改消息報頭,也就是說該使用消息契約了。讀老A的書,這章的確讓我犯暈,上來全是原理,其中從第232頁到第260頁的原理已經給我這個初學SOA的新手扯暈了,看來之後講東西千萬不能上來就扯原理啊!既然根本記不住,那麼就直接來寫代碼吧!

  首先改了上面例子裏的數據契約,換成消息契約:

[MessageContract]
public class Order
{
    [MessageHeader]
    public SoapHeader header;
    [MessageBodyMember]
    public SoapBody body;
}
    
[DataContract]
public class SoapHeader
{
    [DataMember]
    public Guid Id { get; set; }
}

[DataContract]
public class SoapBody
{
    [DataMember]
    public DateTime Date { get; set; }
    [DataMember]
    public string Customer { get; set; }
    [DataMember]
    public string ShipAddress { get; set; }
    [DataMember]
    public double TotalPrice { get; set; }
}

  消息契約是用MessageContract標籤修飾的,咱們要控制的消息頭用MessageHeader修飾,消息體則由MessageBodyMember修飾,這樣一來就把消息頭和消息體拆分開來能夠獨立變化了。而後,修改服務契約:

[ServiceContract]
public interface IOrderService
{
    [OperationContract]
    void ProcessOrder(Order order);
}

這裏將方法的參數設置爲了消息契約的對象,須要注意的是若是使用消息契約,則參數只能傳一個消息契約對象,不能使用多個,也不能和數據契約混用。

  接下來發布服務,在客戶端編寫以下代碼:

static void Main(string[] args)
{
    OrderServiceClient proxy = new OrderServiceClient();

    SoapHeader header = new SoapHeader();
    header.Id = Guid.NewGuid();

    SoapBody body = new SoapBody();
    //body.Date = DateTime.Now;
    //body.……

    proxy.ProcessOrder(header, body);
    Console.ReadKey();
}

  消息契約第一個典型應用就是在執行文件傳輸時,文件的二進制信息放到body裏,而一些復加的文件信息則放在head裏。

  寫完代碼以後來看看這些標籤的屬性。MessageContract標籤的IsWrapped屬性表示是否將消息主體整個包裝在一個根節點下(默認爲true),WrapperName和WrapperNamespace則表示這個根節點的名稱和命名空間。ProtectionLevel屬性控制是否對消息加密或簽名。

  MessageHeader有一個MustUnderstand屬性,設定消息接收方是否必須理解這個消息頭,若是沒法解釋,則會引起異常,這個值能夠用來作消息契約的版本控制。

  MessageBody當中有一個Order順序屬性,它不存在於MessageHeader當中,是由於報頭是與次序無關的。

  

  十6、消息編碼

  SOAP當中的XML是通過編碼後發送出去的,WCF支持文本、二進制和MTOM三種編碼方式,分別對應XmlUTF8TextWriter/XmlUTF8TextReader、XmlBinaryWriter/XmlBinaryReader和XmlMtomWriter/XmlMtomReader。

  選擇哪種編碼方式取決於咱們的綁定,UTF8編碼沒什麼好解釋的,BasicHttpBinding、WSHtpBinding/WS2007HttpBinding和WSDualHttpBinding在默認狀況下都使用這種編碼。

  若是XML很大,則應該使用二進制的形式,二進制編碼會將XML內容壓縮傳輸。NetTcpBinding、NetNamedPipeBinding和NetMsmqBinding都採用這種編碼。

  對於傳輸文件這樣的大規模二進制傳輸場合,應該採用MTOM模式。

  若是咱們須要本身改寫編碼的方式就須要改綁定的XML或者手寫綁定了(P285)。

 

   十7、異常與錯誤契約

  接下來換另外一個話題——異常處理。和普通服務器編程同樣,在WCF的服務端也是會引起異常的,好比在服務器端除了一個0,這時候異常會拋出到服務器端,那麼既然WCF是分佈式通訊框架,就須要把異常信息發送給調用它的客戶端。

  若是把異常的堆棧信息直接發送給客戶端,顯然是很是危險的(不解釋),因此通過WCF的內部處理,只會在客戶端拋出「因爲內部錯誤,服務器沒法處理該請求」的異常信息。

  若是確實須要把異常信息傳遞給客戶端,則有兩種方式,一種是在配置文件裏將serviceDebug行爲的includeExceptionDetailInFaulte設置爲true,另外一種手段就是在操做契約上增長IncludeExceptionDetailInFaulte=true的服務行爲反射標籤,具體代碼與前面相似。

  在這種設置之下,拋出的異常的類型爲FaultException<TDetail>,這是個泛型類,TDetail在沒有指定的狀況下是ExceptionDetail類,因而在客戶端咱們能夠如此捕獲異常:

CalculatorServiceClient proxy = new CalculatorServiceClient();
int result;

try
{
    result = proxy.Div(10, 0);
    Console.WriteLine(result);
}
catch (FaultException<ExceptionDetail> ex)
{
    Console.WriteLine(ex.Detail.Message);
    (proxy as ICommunicationObject).Abort();
}

在處理異常以後,須要手動關掉服務代理。

  固然,能夠事先在服務器端定義好一些異常,用來直接在客戶端來捕獲非泛型的異常:

public int Div(int num1, int num2)
{
    if (num2 == 0)
    {
        throw new FaultException("被除數不能爲0!");
    }
    return num1 / num2;
}
CalculatorServiceClient proxy = new CalculatorServiceClient();
int result;

try
{
    result = proxy.Div(10, 0);
    Console.WriteLine(result);
}
catch (FaultException ex)
{
    Console.WriteLine(ex.Message);
    (proxy as ICommunicationObject).Abort();
}

  可是從習慣上來說咱們喜歡把異常封裝成一個含有其餘信息的對象序列化返回給客戶端,顯而易見這個對象必定要是一個數據契約,首先定義一個數據契約來記錄出錯的方法和消息:

[DataContract]
public class CalculatorError
{
    public CalculatorError(string operation, string message)
    {
        this.Operation = operation;
        this.Message = message;
    }

    [DataMember]
    public string Operation { get; set; }
    [DataMember]
    public string Message { get; set; }
}

以後在服務端拋出,這裏的泛型類就是承載錯誤的數據契約的類型:

if (num2 == 0)
{
    CalculatorError error = new CalculatorError("Div", "被除數不能爲0!");
    throw new FaultException<CalculatorError>(error, error.Message);
}

如此作還不夠,還須要給會拋出這種異常的操做加上「錯誤契約」:

[ServiceContract]
public interface ICalculatorService
{
    [OperationContract]
    [FaultContract(typeof(CalculatorError))]
    int Div(int num1, int num2);
}

如此就能在客戶端捕獲具體泛型類的錯誤了:

try
{
    result = proxy.Div(10, 0);
    Console.WriteLine(result);
}
catch (FaultException<CalculatorError> ex)
{
    Console.WriteLine(ex.Detail.Operation);
    Console.WriteLine(ex.Detail.Message);
    (proxy as ICommunicationObject).Abort();
}

  須要注意的是,一個操做能夠打多個錯誤契約標記,可是這些錯誤契約的名稱+命名空間是不能重複的,由於自定義的錯誤類型會以WSDL元數據發佈出去,若是有重複的名稱,就會發生錯誤(下P17)。

  同時,WCF也支持經過標籤方式將錯誤類採用XML序列化,這裏再也不贅述(下P18)。

相關文章
相關標籤/搜索