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)。

相关文章
相关标签/搜索