上一篇:WCF把书读薄(2)——消息交换、服务实例、会话与并发

  十二、数据契约

  在实际应用当中数据不可能仅仅是以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)。

  DataMember有四个属性,Name为序列化后在XML中的节点名称,Order为在XML中的排序,默认为-1,从小到大排序,在我们队序列化后的结果不满意时可以通过这个属性进行修改,序列化后的数据规则是:父类在前之类在后,同一类型中的成员按照字母排序,IsRequired表示属性成员是否是必须成员,默认为false可缺省的,EmitDefaultValue表示该值等于默认值时是否序列化,默认为true。

  在应用当中服务可能来回传递很大的DataSet,导致服务器端序列化不堪重负,于是可以修改WCF服务行为的maxItemInObjectGraph的值来控制最大序列化对象的数量上限,比如设置为2147483647(P178)。如何设置服务行为这里不再赘述,可以看我的上一篇笔记。

  SOAP消息里的内容是使用DataContractSerializer序列化的,当然,如果想换一种序列化方式,可以在服务契约类上打标签比如[XmlSerializerFormat]。

  

  十三、继承关系的序列化

  依旧是老A的例子,假设有如下的数据契约和服务:

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);
}
}

这里通过一个类来反射获取已知类型。

  十四、数据契约的版本控制

  不论服务端还是客户端,他们的之间发送的数据都是要序列化为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" />

  十五、消息契约

  其实利用数据契约已经能够很好地完成数据的传输了,而数据契约只能控制消息体,有时候我们想在数据传递过程中添加一些额外信息,而不希望添加额外的契约字段,那么我们就得改消息报头,也就是说该使用消息契约了。读老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当中,是因为报头是与次序无关的。

  

  十六、消息编码

  SOAP当中的XML是经过编码后发送出去的,WCF支持文本、二进制和MTOM三种编码方式,分别对应XmlUTF8TextWriter/XmlUTF8TextReader、XmlBinaryWriter/XmlBinaryReader和XmlMtomWriter/XmlMtomReader。

  选择哪一种编码方式取决于我们的绑定,UTF8编码没什么好解释的,BasicHttpBinding、WSHtpBinding/WS2007HttpBinding和WSDualHttpBinding在默认情况下都使用这种编码。

  如果XML很大,则应该使用二进制的形式,二进制编码会将XML内容压缩传输。NetTcpBinding、NetNamedPipeBinding和NetMsmqBinding都采用这种编码。

  对于传输文件这样的大规模二进制传输场合,应该采用MTOM模式。

  如果我们需要自己改写编码的方式就需要改绑定的XML或者手写绑定了(P285)。

  十七、异常与错误契约

  接下来换另一个话题——异常处理。和普通服务器编程一样,在WCF的服务端也是会引发异常的,比如在服务器端除了一个0,这时候异常会抛出到服务器端,那么既然WCF是分布式通信框架,就需要把异常信息发送给调用它的客户端。

  如果把异常的堆栈信息直接发送给客户端,显然是非常危险的(不解释),所以经过WCF的内部处理,只会在客户端抛出“由于内部错误,服务器无法处理该请求”的异常信息。

  如果确实需要把异常信息传递给客户端,则有两种方式,一种是在配置文件里将serviceDebug行为的includeExceptionDetailInFaulte设置为true,另一种手段就是在操作契约上增加IncludeExceptionDetailInFaulte=true的服务行为反射标签,具体代码与前面类似。

  在这种设置之下,抛出的异常的类型为FaultException<TDetail>,这是个泛型类,TDetail在没有指定的情况下是ExceptionDetail类,于是在客户端我们可以如此捕获异常:

CalculatorServiceClient proxy = new CalculatorServiceClient();
int result; try
{
result = proxy.Div(, );
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 == )
{
throw new FaultException("被除数不能为0!");
}
return num1 / num2;
}
CalculatorServiceClient proxy = new CalculatorServiceClient();
int result; try
{
result = proxy.Div(, );
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 == )
{
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(, );
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)。

WCF把书读薄(3)——数据契约、消息契约与错误契约的更多相关文章

  1. WCF把书读薄(2)——消息交换、服务实例、会话与并发

    上一篇:WCF把书读薄(1)——终结点与服务寄宿 八.消息交换模式 WCF服务的实现是基于消息交换的,消息交换模式一共有三种:请求回复模式.单向模式与双工模式. 请求回复模式很好理解,比如int Ad ...

  2. WCF把书读薄(4)——事务编程与可靠会话

    WCF把书读薄(3)——数据契约.消息契约与错误契约 真不愧是老A的书,例子多,而且也讲了不少原理方面的内容,不过越读越觉得压力山大……这次来稍微整理整理事务和可靠会话的内容. 十八.事务编程 WCF ...

  3. [WCF编程]11.错误:错误契约

    一.错误传播 服务需要向客户端报告特定错误,当WCF默认的错误屏蔽方法并不包含这一实现.另一个重要的问题与传播到客户端有关,即由于异常是针对特定技术的,因此无法跨越服务边界而被共享.要实现无缝的互操作 ...

  4. WCF技术剖析之十八:消息契约(Message Contract)和基于消息契约的序列化

    原文:WCF技术剖析之十八:消息契约(Message Contract)和基于消息契约的序列化 [爱心链接:拯救一个25岁身患急性白血病的女孩[内有苏州电视台经济频道<天天山海经>为此录制 ...

  5. 【IPC进程间通信之四】数据复制消息WM_COPYDATA

    IPC进程间通信+数据复制消息WM_COPYDATA                IPC(Inter-Process Communication,进程间通信).         数据复制消息WM_C ...

  6. WCF传输过大的数据导致失败的解决办法

    WCF传输过大的数据导致失败的解决办法   WCF服务默认是不配置数据传输的限制大小的,那么默认的大小好像是65535B,这才65KB左右,如果希望传输更大一些的数据呢,就需要手动指定一下缓冲区的大小 ...

  7. WCF分布式开发步步为赢(15):错误契约(FaultContract)与异常处理(ExceptionHandle)

    今天学习WCF分布式开发步步为赢系列的15节:错误契约(FaultContract)与异常处理(ExceptionHandle).本节内容作为WCF分布式开发的一个重要知识点,无论在学习还是项目中都应 ...

  8. 使用Fiddler解析WCF RIA Service传输的数据

    原文 http://www.cnblogs.com/wintersun/archive/2011/01/05/1926386.html 使用Fiddler 2 解析WCF RIA Service传输的 ...

  9. 大数据平台消息流系统Kafka

    Kafka前世今生 随着大数据时代的到来,数据中蕴含的价值日益得到展现,仿佛一座待人挖掘的金矿,引来无数的掘金者.但随着数据量越来越大,如何实时准确地收集并分析如此大的数据成为摆在所有从业人员面前的难 ...

随机推荐

  1. 04:sqlalchemy操作数据库 不错

    目录: 1.1 ORM介绍(作用:不用原生SQL语句对数据库操作) 1.2 安装sqlalchemy并创建表 1.3 使用sqlalchemy对表基本操作 1.4 一对多外键关联 1.5 sqlalc ...

  2. photoshop画矩形款

    1.屏幕截图 2.在photoshop新建图形 3.用矩形选框 4.右键打开描边,宽度设成5个像素

  3. 三种实现Ajax的方式

    本文主要是比较三种实现Ajax的方式 1. prototype.js 2. jquery1.3.2.min.js 3. json2.js Java代码 收藏代码 后台处理程序(Servlet),访问路 ...

  4. 字符串(PHP学习)

    1.什么是字符串 答:一串字符组成(参考羊肉串) 2.字符串定义 答:单引号,双引号,包含单引号或双引号的字符串(1.双引号里面有单引号2.单引号里面有双引号3.转义4.字符拼接) 3.单双引号定义字 ...

  5. Jenkins的用户管理

    用户管理入口 Jenkins首页有一个用户,但是只能从那查看用户列表和信息,管理用户的入口在Jenkins->系统管理->管理用户 新建用户 在管理用户左侧有一个新增用户,点击后按照表单填 ...

  6. Jmeter录制HTTPS 补充

    Jmeter有录制功能,录制HTTPs需要增加一个证书配置,录制步骤如下: 1.打开jmeter,添加线程组.线程组右键,逻辑控制器>录制控制器 工作台 右键 非测试元件 >HTTP代理服 ...

  7. 深入理解mysql索引

    深入理解mysql索引 1 深入理解索引 1.1 索引基础理论知识: 1.2 B+树索引 1.3 哈希索引 1.4 理解B+树.哈希索引结构及区别: 1.5 理解常见索引的基本概念:主键索引.唯一索引 ...

  8. JavaScript笔记——基础知识(一)

    <Script>标签属性 <script>xxx</script>这组标签,是用于在 html 页面中插入 js 的主要方法.它主要有以下 几个属性: charse ...

  9. Deep Learning 学习笔记(8):自编码器( Autoencoders )

    之前的笔记,算不上是 Deep Learning, 只是为理解Deep Learning 而需要学习的基础知识, 从下面开始,我会把我学习UFDL的笔记写出来 #主要是给自己用的,所以其他人不一定看得 ...

  10. jqurey datatable tableTools 自定义button元素 以及按钮定义事件

    版本 1.10.4 "dom": 'T<"clear">lfrtip', "tableTools": { //"sSw ...