WCF4.0进阶系列--第六章 维护服务协定和数据协定

【摘要】

在第一章WCF简介中,你已经了解SOA的基本原则--服务之间共享数据架构和协定,而并非类或者类型。当你定义服务时,你通过定义服务协定以指定操作。服务协定描述该服务的操作、操作所使用的参数类型及其操作返回值的类型。WCF服务对外公布服务协定的定义,服务开发人员使用这些定义去构建相应的客户端。开发人员可以通过Visual Sutdio自带的添加服务向导或者svcutil实用工具为根据服务的WSDL描述生成一个客户端代理类,客户端使用该代理类与进行服务通信。

服务协定仅仅是整个故事的一部分。服务协定中的操作可以使用参数并且操作可以有返回值。客户端程序必须提供服务所要求格式的数据。在.NET Framwork中,主要数据类型都有预先定义的格式;而类,结构,枚举等类型则拥有比较复杂的格式;这些复杂格式的数据类型要求客户端程序在想服务发送消息时,先将这些复杂数据打包然后才发送。同样地,服务端也需要格式化这些复杂数据,然后才发送给客户端。你可以使用数据协定来封装这些复杂格式的数据类型;服务所使用的复杂数据类型都应有一个对应的数据协定。服务将数据协定和服务协定一起对外公布,那么这些复杂格式的数据类型将包含在通过svcutil实用工具或visual studio添加服务向导所生产的客户端代理类中。

服务协定和数据协定是WCF服务非常基础的部分;如果客户端不能识别服务对外公布的操作或者服务所使用的数据类型,那么该客户端在与服务进行通信时将遇到很多麻烦。

【目录】

更改服务协定

   选择性地保护操作

  服务的版本

  对服务协定实施中断行更改和非中断性更改

更改数据协定

  数据协定特性和数据成员特性

  数据协定的兼容性

【正文】

修改服务协定

服务协定就是一个接口,WCF工具和架构能将该协定转换成一个WSDL文档,在该文档中列出一个服务的所有操作(一系列SOAP消息和消息响应)。你在服务实现类中实现这些接口中的方法。当WCF服务开始运行时,WCF运行时使用服务配置文件中的绑定创建一个通道堆栈,然后开始侦听来自客户端的消息请求。WCF运行时将来自WCF客户端的消息转换成方法的调用并且调用服务实现类实例中对应的方法。该方法返回的数据将转换成一个SOAP消息然后回传给通道堆栈,最后通道堆栈传将该消息传输至客户端。

从上述过程,可以得出两个结论:

(1). 服务协定独立于服务端的通信。根据服务配置文件中绑定而创建的通道堆栈管理通信机制。这就意味着,即使变更了通信传输协议或者服务的地址,也不需修改服务或者访问该服务客户端程序的代码。更进一步讲,服务的安全方面也独立于服务协定。

(2). 与服务通信的客户端程序必须构建与服务向适应的SOAP消息。这些消息依赖于服务协定。如果服务协定变更,客户端必须也提供新版本的SOAP消息。否则,服务端将不能识别客户端发送的消息或者客户端想服务发送格式不正确的SOAP消息。此外,如果服务返回的消息发生变更,客户端程序很可能也不能处理这些返回消息。

你可以从本章后续的练习中来验证上述两点结论。

可选择性的保护操作

在第四章和第五章讲述了如何保护客户端和服务端传送的消息。但是,你所使用的技术侧重于通过绑定和服务的行为来实现整个服务的安全(即一个服务的所有操使用同一个安全设置)。而通过修改服务协定,你可以在同一个服务中根据不同的安全需求对各个操作设定对应的安全性。

根据安全需求设定WCF服务的操作的安全性

1. 使用Visual Studio打开*\Chapter6\ProductsService文件夹下的ProductsServcie方案。该方案复制了第四章的ProductsServcie解决方案。该解决方案包含了ProductsServiceLibrary项目、ProductsServcieHost项目和ProductsClient项目;在本章中,该服务将使用非SSL端点,并且该端点WS2007HttpBinding绑定。WS2007HttpBinding绑定默认实现了消息安全并且使用Windows token来验证客户端用户。

2. 打开ProductsServiceLibrary项目下的IProducts.cs文件;添加下面的using语句

using System.Net.Security;

3. 修改ListProducts和GetProduct方法

OperationContract特性类的ProtectLevel属性用于指定如何保护调用该操作的消息(即规定了客户端发送至该操作消息的安全特性)。在本例中,EncryptAndSign指定调用的ListProducts和GetProduct操作的消息必须由客户端签名、并使用与服务协商的密钥加密。这就要求服务端和客户端采用的绑定必须设置其安全模式为消息安全验证,此外客户端和服务端还必须为AlgorithmSuite属性指定同样的值。 实际上,上述设置是WS2007HttpBinding绑定使用消息安全时的默认设置。此外,ProtectLevel还可能有下列值:

请注意ProtectLevel属性的默认值还取决于端点所使用的传输协议。如果使用BasicHttpBinding,那么默认的值将为None。

4. 修改CurrentStockLevel和ChangeStockLevel的OperationContract特性

5. 使用WCF服务配置工具打开ProductsServiceHost的app.config文件

6. 点击"Disgnostics",确认右边面板中的MessageLogging的状态为On

7. 点击"Listeners—Messagelog", 修改InitData的路径为"*\Chapter6\ProductsService"

8. 点击"Diagnostics—Message logging",在右边面板中设置LogMessageAtServiceLevel属性的值为false,确认LogMessageAtransportLevel属性的值为true

9. 保存配置文件,并退出WCF服务配置工具。

测试修改后的服务

1. 运行ProductsServcieHost,点击"开始"按钮启动ProductsServcie服务

2. 启动ProductsClient,你将得到如下结果:

Test1和test2都成功完成,因为绑定实现了加密和签名,这符合定义在ListProducts和GetProduct方法上的操作协定。但是test3抛出了一个异常:"The primary signature must be encrypted",因为CurrentStockLevel操作在操作协定中设定了保护级别为签名,但是客户端的绑定根据默认的设置提供了加密和签名。产生该问题的原因就是你更新了服务协定,但是没有更细客户端代码中相应的代码。所以客户端的代理在test3中仍旧向服务发送加密和签名后的消息。

3. 退出ProductsClient

4. 在Visual Studio中,修改ProductsClient项目下的Products.cs文件

5. 删除位于*\Chapter6\productsService文件夹下的ProductsService.svclog

6. 打开ProductsServcieHost,然后点击"开始"按钮启动ProductsServcie

7. 启动ProductsClient,然后你会得到如下结果:

8. 关闭ProductsClient

9. 启动"Service trace viewer",并打开位于*\Chapter6\productsService文件夹下的ProductsService.svclog

10. 在服务追踪查看器中,点击左边面板中的"消息"标签。你可以看到六条消息关于客户端和服务之间的协商加密的密钥,它们的Action都使用http://docs.oasis-open.org命名空间。 在这六条消息之后,是8条服务端和客户端收发的消息,它们的Action都是使用http://tempuri.org命名空间。

11. 选中http://tempuri.org/IProductsService/ListProducts, 在后边面板中,点击"Formatted"标签,并滚动到内容的底端,你可以看到"Envelope Information section"的内容。你可看到Method对应的值为e:EncryptedData;由此可见,客服端发送给服务的操作名已经加密

11. 选中http://tempuri.org/IProductsService/ListProductsResponse, 在右下面板中,确认服务返回的消息也已经加密。确认http://tempuri.org/IProductsService/GetProduct 和ttp://tempuri.org/IProductsService/GetProductResponse的消息也被加密

12. 选中http://tempuri.org/IProductsService/CurrentStockLevel,同样切换到右下的"Formatted"标签下,你可以发现客户端向服务发送的操作名为及该操作的参数都没加密。

13. 检查http://tempuri.org/IProductsService/CurrentStockLevelResponse、http://tempuri.org/IProductsService/ChangeStockLevel以及http://tempuri.org/IProductsService/ChangeStockLevelResponse;确认它们都操作的名字和参数或返回结果没有被加密。

14. 关闭ProductsService.svclog;并退出Service trace viewer

服务版本管理

更改时刻发生。一个被广泛使用的服务几乎不可避免的随着业务过程的变化而演化。那么发生的更改可能有哪些方式呢?

  • 在大多数情况下,通过服务实现类中修改代码;
  • 有时候,通过修改服务操作的定义;
  • 当然,可以通过添加新的服务操作;
  • 或者通过删除一些不再使用的或重复的操作;
  • 或者变更一些现有操作的参数类型或返回类型。

显然,这些更改都会导致更新服务协定。然后,客户端通过服务协定指定向服务收发的消息。如果服务协定变更,而客户端仍使用之前版本的服务协定,那么将导致什么结果? 是客户端能继续工作,还是需要检查每个客户端并更新客户端的代码呢? 你是否知道每个客户端的具体位置? 因为客户端可能通过因特网连接到服务,所以这些客户端可能分布在世界上的任何地方。 你可以看到,修改一个服务并不是话费稍许的气力就可以实现的。从上述情况来看,你需要采取多个步骤以确保现有的客户端即使没有更新仍能继续工作。 下面的练习展示了一些常见的场景,可以帮助理解当服务或服务协定变更时实际发生了什么;以及为减少服务变更带来的负面影响所应遵循的策略。

为WCF服务添加一个方法和修改方法的业务逻辑

1. 使用Visual Studio打开ProductsServiceVersion方案下ProductsServcieLibrary项目中的ProductsServcie.cs文件

2. 添加下面方法(检查一个产品是否存在)到服务实现类中

3. 修改GetProduct方法的业务逻辑

4. 修改CurrentStockLevel的业务逻辑

5. 修改ChangeStockLevel的业务逻辑

6. 重新生成项目ProductsServiceLibrary;然后打开ProductsServiceHost,点击"开始"运行ProductsServcie;并打开ProductClient,所有的操作都可以成功执行。

7. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

8. 在Visual Studio中,按照下图中红框中的内容修改program.cs文件中的test2;

9. 再次启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。你将得到如下结果:

10. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

11. 再次编辑program.cs文件;修改test2的方法使用一个存在的productnumber

从上面的练习我们可以看到,尽管服务发生变更,并且为该服务添加了一个新方法。但是客户端的服务协定没有更新;因此客户端程序不能访问ProductExist方法;客户端仍旧与使用之前一样的方式访问服务。上述变更是一个非中断性服务变更。

服务协定中现有的操作添加一个参数

1. 打开ProductsServcieLibrary项目下的IProductsServcie.cs

2. 添加一个参数 到ListProducts操作

3. 打开ProductsServcieLibrary项目下的ProductsServcie.cs,修改ListProducts方法:有两处修改,第一处为ListProducts方法添加参数;第二处为修改LINQ方法,使其根据参数过滤返回的产品编码列表。

4. 重新生成解决方案后,启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试都将通过,而且ListProducts将返回所有的产品。

从上图可以看到,test1方法,也就是ListProducts并未返回任何值。因为客户端仍然使用没有参数的ListProducts方法向服务发送消息;当宿主WCF服务的WCF运行时接收到该消息后,反序列化消息,然后发现该消息的body部分没有包含参数数据,然后WCF运行时向服务传输一个null的参数值。服务的ListProducts方法将相应地返回一个空的列表。

5. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

你可能非常奇怪上面练习的结果,你发现你可以对一个操作添加一个参数(或从一个操作移除一个参数),现有的客户端仍然可以调用该操作。如果你对一个操作添加一个参数,并且客户端程序没有为该操作提供参数,那么WCF运行时根据参数的数据类型为其提供该数据类型的默认值:引用类型为null,值类型为0;Boolean为false。请注意,即使你在定义操作时,设定了参数的值;但当客户端调用该操作时如果没有提供参数,那么这些默认值(null,0和false)仍然传至所调用的方法,并且覆盖你在服务中设定的参数值;比如你按照下列方式定义ListProducts

如果客户端调用该方法时没有提供参数值,那么WCF运行时将为ListProducts方法的参数设置为null。

同样地,如果你在客户端变更了参数的类型,那么当该参数传递值寄宿WCF服务的WCF运行时后,WCF运行时将尝试类型转化。如果转化失败,那么将抛出异常。

添加、移除和修改服务协定中操作参数是不值得推荐地,如果发生上述情况,你应该格外小心这些变更导致的结果。比如,如果移除某个操作包含的多个参数中间某一个参数,那么由客户端传送至服务端的参数在反序列化时可能会反序列化为错误参数。

如果你变更一个操作的返回类型。当服务返回的类型不能转化成客户端程序希望的类型,那么客户端程序处理消息所使用的格式化工具将抛出一个异常。

一般地,你应当避免修改操作的参数类型或者参数个数。相反地,在你修改服务协定后,保证新客户端工作的同时,应确保与现有客户端的兼容性。 实际上,你应该定义一个新版本的服务协定,并且保存之前版本的服务协定。在下面的练习中你完成上述目的。

为WCF服务添加一个新操作

1. 打开ProductsServcieLibrary项目下的IProductsServcie.cs

2. 在接口IProductsService中,从ListProducts方法中移除match参数,并添加另外一个版本的ListProducts方法。

请注意,虽然C#支持方法重载,但是SOAP标准不允许一个服务对外公布多个具有相同名字的操作。但是这里有另外一种方法,可以让你在使用重载的同时还支持SOAP;那么就是在相同方法名字上设置OperationContract特性的Name属性值以在SOAP消息中生成不同的操作名。比如

3. 打开ProductsService.cs,做如下修改:

4. 重新生成解决方案后,启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;并且ListProducts将返回所有的产品。

5. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

为服务协定所添加新方法并是一个非中断变更。如果你构建一个新的WCF客户端程序,你可以使用svcutil施工工具生成一个包含该新操作的代理类。现有的客户端程序使用老版本代理类仍然可以和服务端通信,而且老版本的代理类不关心新操作的存在。

但是,如果你希望新的代理类仅仅使用新的操作(ListMatchedProducts)而不能使用旧的操作(ListProducts)时,将带来潜在的问题。该如何对新版本的客户端程序隐藏旧的操作呢?答案就是使用多重服务协定。保持现有的服务协定不做任何变更,新版本的服务协定仅仅包含新操作。如下表所示:

老版本服务协定

新版本服务协定

上面的代码展示了使用ServiceOperation特性类的命名空间属性和Name属性来区分和命名不同的服务版本。默认情况下,服务协定使用http://tempuri.org命名空间,使用接口的名字来命名来服务协定。当你定义一个新版本的服务协定时,可以通过命名空间属性值来区分不同的服务协定,但仍保持相同的Name属性。 请注意,设置命名空间属性和Name属性后,将导致一个中断性变更,因为这些信息用以识别服务和客户端之间收发的SOAP消息。

如果你定义了两个不同的版本的服务,那么此时你的服务实现类需要同时实现这两个接口。

最后,你需要在服务端创建一个使用新版本服务的服务端点,并且该端点使用ProductsServcie.IProductsServcieV2服务协定。

对服务协定创建中断和非中断性变更

严格地讲,你应该考虑服务的稳定性。服务协定的任何变化都很可能影响到客户端程序,导致客户端不再能继续与服务进行正常通信。在实际中,你可以对服务协定做一些变更,且不中断现有的客户端程序通过之前的服务协定连接到该服务。下表列举了一些开发人员经常对服务协定所作的变更和这些变更对客户端程序所带来的影响

变更

影响

添加新操作

非中断变更。现有客户端不受影响,但是使用老版本代理类的客户端程序看不到新操作。但是使用代码创建查询和消息的客户端可以使用新的操作,详细内容参考第十一章

移除一个操作

中断变更。现有客户端将不能继续正常工作;尽管不调用该操作客户端不受影响

变更操作名

中断变更。现有客户端将不能继续正常工作;尽管不调用该操作客户端不受影响。请注意,操作名默认对应服务协定中的方法名。你可以变更服务协定中的方法名;但应保留操作名。比如你可以通过下列方式来完成:

这是一个不错的方法,因为它移除了服务协定和实现该操作的名字之间的依赖关系。

修改操作的保护级别

中断变更。现有客户端将不能再调用该操作

对操作添加新参数

非中断操作。现有客户端可以继续调用该操作,但是WCF运行时将为根据新参数的类型为新参数提供其类型对应的默认值

变更操作参数的顺序

中断操作。结果不易预料(某些现有客户端还能继续保持工作)

移除操作的一个参数

可能是非中断操作,只要被移除的参数是该操作的最后一个参数。在这种情况下,现有客户端传递过来的多余参数将被忽略。如果被移除的参数为第一个或中间的参数,那么将导致一个中断操作,引用于变更参数的顺序相同

变更操作参数的类型
或变更返回值类型

可能是中断操作,如果WCF格式化工具不能将之前的数据类型转化成新的数据类型。现有客户端可能继续工作,但是将导致SOAP消息中数据丢失或者错误地转化的风险。该变更还会影响到应用或移除ref和out参数的服务协定,及时ref和out的参数类型没有发生变更。更多消息请参考本章后续内容"修改数据协定"

添加FaultOperation特性

中断变更。现有客户端程序能发送fault消息,但是这些消息不能正确的理解

移除FaultOperation特性

非中断变更。现有客户端将可以继续正常工作,尽管指定的异常处理被认为过期

变更服务协定的命名空间属性和Name属性

中断变更。使用之前的Name或者命名空间现有客户端程序将不能再向服务发送消息。

如果你对服务协定创建了一个中断性变更,你必须更新所有访问该服务的客户端程序。如果客户端程序使用代理,你需要更新这些代理。推荐的修改服务协定的方式是:创建一个新版本的服务协定,并同时保持旧版本的服务协定。这种方式你不需要更新现有的客户端程序,尽管它们不使用服务的新操作。

修改数据协定

服务协定中的方法能使用参数,并且返回值;这些参数和返回值包含在SOAP消息中并在服务和客户端之间传输。SOAP消息将这些数据编码成XML文本。WCF运行时使用内建的XML序列化器将这些XML文本序列化和反序列化为.NET Framework的主要数据类型,比如整数,数字,或者字符串。对于更复杂的接口类型,服务必须指定确切的序列化格式;可以用多种方式将同一个结构化数据描述成不同XML。你使用数据协定来定义结构化的类型。WCF运行时使用数据协定来序列化和反序列化这些结构化的类型。

使用数据协定,你可以准确地为服务指定XML格式的数据。在WCF客户端,数据协定序列化器使用数据协定将参数数据序列化为XML。在WCF服务端,数据协定序列化器使用数据协定将XML数据反序列化为其能处理的参数数据。服务的返回值同样地在服务端序列化为XML数据,传送至客户端后再被反序列化。

数据协定特性和数据成员特性

在第一章,你已经看到如何定义一个简单的数据协定,用以呈现返回给客户端的产品数据。这就是一个数据协定的具体模样:

为一个类添加DataContract特性后,那么该类将可以被数据协定序列化器格式化。数据协定序列化器将序列化和反序列化该类中标记了DataMember特性的成员。ProductData中的成员都是.NET Framework主要的数据类型,序列化器使用内建的规则将这些成员转换成XML消息。比如下面就是ProudctData序列化后的XML片段:

如果ProductData类中包含结构类型的成员,那么该成员也应标记DataContract特性。相应地,序列化器将对这些成员嵌套使用序列化和反序列化过程。

DataContract和DataMember特性具有可选属性,你可以使用这些属性来调整序列化器工作的方式。你将在下面的练习中研究这些属性。

通过数据协定变更序列化后ProductData成员的顺序

1. 打开解决方案*\Chapter6\ProductsServiceVersion

2. 打开ProductsServiceLibrary下的IProdutsService.cs,找到ProductData类,你可以看到该类有四个成员,他们是Name,ProductNumber,Color和ListPrice

3. 使用WCF服务配置工具,打开ProductsServiceHost项目下的app.config; 变更"诊断—侦听器—消息日志"的InitData属性的值到*\Chapter6\ProductsServiceVersion文件夹下

4. 保存设置,并退出WCF服务配置工具

5. 使用资管管理器,转到目录*\Chapter6\ProductsServiceVersion,删除ProductsService.svclog

6. 启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;并且ListProducts将返回所有的产品。

7. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

8. 回到目录*\Chapter6\ProductsServiceVersion,双击ProductsService.svclog。以使用服务追踪查看器打开该日志

从GetProductResponse中,我们可以看到ProductData的序列化为XML后,成员的顺序为Color, ListPrice, Name和ProductNumber(按照字母顺序排列)

9. 关闭服务追踪查看器

10. 回到Visual Studio,按照下列方式编辑IProductsServcie

并不是变更成员的名字以实现序列化后的顺序,而是通过指定DataMember特性类的Order属性值来指定一个数字序列。数据协定序列化器将根据Order属性的值来顺利地序列化ProductData成员,数字值小的先序列化;如果两个成员Order值相同,那么再按照字母顺序排列 。

11. 使用资管管理器,转到目录*\Chapter6\ProductsServiceVersion,删除ProductsService.svclog

12. 启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;并且ListProducts将返回所有的产品。

13. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

14. 回到目录*\Chapter6\ProductsServiceVersion,双击ProductsService.svclog。以使用服务追踪查看器打开该日志

我们可以看到ProductData的成员按照我们指定的顺序被序列化。

注意:为了使上述代码可以正常工作,你需要重新生成客户端代理类。但在做这个任务前,我们先来了解一下如果变更成员的名字,将会对数据协定带来什么影响?

与服务协定相似,数据协定序列化器使用数据成员的名字形成每个序列化后字段的名字。想应地,变更数据成员的名字会导致中断性变更,其要求更新客户端程序。与服务协定中的操作一样,你可以为数据成员提供一个逻辑名字,数据协定序列化器将使用该名字来替换数据成员的实际名字。DataMember特性类提供了一个Name的属性,你可以通过该属性来为数据成员指定逻辑名,这样你即使更换了实际的名字,逻辑名字仍然可以保持不变。比如:

数据协定特性类提供了NameSpace属性。默认情况下,WCF使用命名空间为http://schemas.datacontract.org/2004/07 加上该数据协定类的命名空间。以IProductsService为例,那么数据协定序列化器将使用的命名空间为http://wchemas.datacontract.org/2004/07/Products. 你可以通过指定Namespace属性的值覆盖默认的命名空间。这种方法是值得推荐的方式。你可以在命名空间里指定一个日期用以辨别数据协定的不同版本。如果你更新了数据协定,那么你紧接着应该通过数据协定特性类的Namespace属性指定该数据协定修改的日期。

修改ProductData数据协定的Namespace属性

1. 在Visual Studio,打开ProductsServiceLibrary项目下的IProductsServcie.cs文件

2. 指定ProductData数据协定特性类的Namespace属性值

3. 资管管理器,转到目录*\Chapter6\ProductsServiceVersion,删除ProductsService.svclog

4. 启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;但是Test2,Name字段没有显示任何值。

5. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

6. 回到目录*\Chapter6\ProductsServiceVersion,双击ProductsService.svclog。以使用服务追踪查看器打开该日志

7. 关闭服务追踪查看器

通过上述练习,你可以看到ProductsService服务按照期望的方式格式化消息,尽管此时客户端程序还不能正确的处理这些消息(客户端代理为更新)。下一步的练习我们将重新生成客户端的代理类。

重新生成代理类,并更新WCF客户端程序

1. 使用管理员身份运行Visual Studio command prompt,然后转到路径*\Chapter6\ProductsServiceVersion\ProductsServiceLibrary\bin\Debug下,

2. 执行命令svcutil.exe ProductsServiceLibrary.dll

执行完该命令后,将生成下列文件

请注意,服务包含两个服务协定,所以上述命令生成了两个WSDL描述文件,每个都有用自己的schema(数据架构)。

3. 输入下列命令从使用描述IProductsServiceV2的WSDL文件和对于的schema文件生成代理类:

注意:如果你想生成IProductsServcie对应的代理类,请使用相应的WSDL文件。

4. 回到Visual studio, 删除product.cs,然后通过添加已存文件的方式,添加上一步生成的ProductsV2.cs文件添加到ProductsClient下,然后修改program.cs:

5. 启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;

6. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

7. 最后,我们再来看一下日志,可以发现,消息的命名空间已经使用最新的服务协定指定的命名空间。

你应该仔细评估数据协定变更导致的影响。更改数据协定会以一种不明显的方式导致客户端程序不能正常工作。原始的SOAP序列化可以为打乱了顺序的或是丢失的字段自动添加该字段类型的默认值。你当然也可以为当前数据协定添加新的成员。在某些情况下,你可以完成该任务而不需要中断现有的客户端程序。你应该注意添加一个成员到数据协定会更改从WCF导出的schema。客户端程序使用该schema去定义与服务收发SOAP消息中数据的格式。 然而,一部分使用其他技术开发的客户端程序会执行严格的schema检查。如果你的服务又必须支持这些类型的客户端程序,你不可以添加数据协定成员后,而不去更新这些客户端。 在这种情况下,与服务版本一样,你应该采取数据协定版本策略。关于数据协定的更多消息,请参考:http://msdn.microsoft.com/en-us/library/ms733832.aspx

在下面的练习中,你将检查添加数据协定成员导致的结果,并且还可以看到WCF客户端如何处理这些新增加的数据成员。

添加一个新成员到ProductData数据协定

1. 转到Visual studio,然后开发ProductsServiceLibrary项目下的IProductsServcie.cs文件

2. 为ProductData添加一个新成员

3. 打开ProductsService.cs中,修改GetProduct方法

4. 使用资管管理器,转到目录*\Chapter6\ProductsServiceVersion,删除ProductsService.svclog

5. 启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;包括test2,新添加的字段没有影响到现有的客户端。

6. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

7. 最后,我们再来看一下日志,可以发现,StandardCost已经包含在服务端的返回消息中。

数据协定序列化器序列化数据协定的所有成员。现有的WCF客户端不仅不关注新字段StandardCost,而且还不执行schema验证,因此客户端直接忽略多余的字段。

8. 退出ProductsService.log,并退出服务追踪查看器

9. 使用svcutil重新生成客户端代理类

10. 回到Visual studio;然后ProductsClient下的ProductsV2.cs文件,然后添加将上一步生成的ProductsV2.cs

11. 修改Programm.cs

12. 重新生成解决方案;然后启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过;包括test2,新添加的字段已经能显示

13. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

添加一个操作到WCF服务并检查数据协定序列化

1. 在Visual Studio中,打开ProductsServiceLibrary项目下的IProductsService.cs文件

2. 按照下列方式修改IProductsServiceV2,添加新操作UpdateProductDetials

3. 重新生成项目ProductsServiceLibrary,在Visual Studio command prompt中 使用svcutil重新生成客户端代理类

4. 回到Visual studio;然后ProductsClient下的ProductsV2.cs文件,然后添加将上一步生成的ProductsV2.cs

5. 修改Programm.cs

6. 重新生成解决方案;然后启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过; 当test5,运行时,将出现一个消息框显示ProductNumber和更新后的ProductName。

7. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

客户端程序使用数据协定定义,成功地将Product对象发送至WCF服务;但是如果客户端程序使用的某个版本的数据协定定义中缺少一个字段,会发生什么?

添加另外一个成员到ProductData数据协定并检查其默认值

1. 在visual studio中,打开ProductsServiceLibrary项目下的IProductsService.cs文件

2. 添加下面的成员(红色矩形内)到ProductData数据协定

3. 修改ProductService.cs类

4. 重新生成解决方案;然后启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。所有的测试方法将成功通过; 当test5,运行时,将出现一个消息框显示FinshedGoodFlag为false。客户端程序仍旧使用旧版本的Product数据协定并且不会为FinsihedGoodFlag赋值。显示为false的原因是数据协定序列号器为其赋予了默认值。点击OK关闭消息提示框。

5. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost

当为数据协定添加新成员时,该新添加的成员会变更数据协定序列号后的顺序,此时,你应当考虑到现有的客户端程序。 如果客户端程序并没有为一个序列化后的对象的每个字段赋值,WCF将使用每个成员的数据类型的默认值,比如boolean类型为false,值类型为0,引用类型为null。如果默认值不可识别,你可以通过序列化回调方法自定义序列化和反序列化过程。 关于自定义序列化的信息,请参考http://msdn.microsoft.com/en-us/library/ms229752.aspx

数据协定的兼容性

如果你需要定义不同版本的数据协定,你需要在确保现有客户端程序兼容性的前提下去实现。DataMember特性提供了两个属性可以帮助你实现数据协定的兼容性

  • IsRequired 如果设置该属性的值为true,那么服务收到的SOAP消息必须包含该成员的值;默认情况下,该属性的值为falseWCF运行时将为任何为提供值的成员根据其数据类型提供该类型的默认值。
  • EmitDefaultValue 如果设置该属性值为true,客户端的WCF运行时将为该成员生产其默认值,如果该成员为包含在客户端发送的SOAP消息中。该属性的默认值为true

如果你需要在一个未来版本的服务中严格地保持数据协定的一致性,你应该为数据协定的每个成员上设置IsRequired属性值为true;并且当你创建第一个版本的服务时应在数据协定的每个成员上设置EmdiDefaultValue属性值为false。如果一个成员在上一个版本中IsRequired为false,你绝对不要在新版本中设置该成员的IsRequired为true。 这样能使数据协定对老版本的客户端保持兼容性。

还有一个进一步的问题需要考虑:客户端程序可能请求遵守服务数据协定的数据,然后修改数据并回传给服务。就好比在ProductsService服务中,先请求GetProduct方法从服务端得到ProductData,随后通过调用UpdateProductDetails将更新后的后的ProductData传回给服务? 如果客户端程序使用未包含新数据协定成员的老版本数据协定,客户端向服务回传数据时会发生什么情况? WCF运行时实现了一项技术叫做"round-tripping"用以确保新的数据成员不会丢失。你将在下面的练习中检查上述情况。

调查WCF运行时如何指定round-tripping

1. 在Visual Studio中,打开ProductsServiceLibrary项目下的ProductsServcie.cs文件

2. 修改GetProduct方法

在代码中,我们设置FinsihedGoodsFlag为true。请记住Boolean类型的默认值为false。

3. 重新生成解决方案;然后启动ServcieHost,点击"开始"运行服务ProductsServcie;然后启动productsClient。你将会得到如下结果:

在消息提示框中,FishishedGoodsFlag的值为true。FinishedGoodsFlag最先由服务端提供,然后它发送至客户端;然后由客户端回传至服务。尽管此时客户端使用老版本的代理(即客户端根本不知道FinishedGoodsFlag字段),客户端WCF运行时管理该字段,并使其保持初始值。 点击OK按钮,关闭消息提示框。

4. 退出ProductsClient;切换到ProductsServiceHost,点击"停止"按钮停止ProductsServcie服务;然后退出ProductsServcieHost 。

WCF运行时通过IExtensibleDataObject接口实现round-tripping。如果你检查ProductsV2.cs文件,你可以看到客户端代理ProductData类实现了该接口。该接口定义了一个名为ExtensionData的属性,该属性的类型为ExtensionDataObject。 ExtensionData属性专为客户端代理而使用,它可以读取和写入数据到一个类型为ExtensionObjectData私有成员extensionDataField,如下面所示:

extensionDataField成员的作用好比一个"水桶",它可以容纳客户端接收到的所有未定义的数据成员;而不是丢弃这些数据成员;代理自动将这些数据成员存贮到该水桶中。当客户端代理向服务回传ProductData对象时,该对象包含了这个水桶中所有的数据成员。 如果你想停止该功能,你可以在客户端的端点行为中,设置dataContractSerializer元素的IgnoreExtensionDataObject属性值为 true。比如下面这样:

你同样可以在服务端停止该特性:

【总结】

在本章,你了解了如何使用服务和数据协定来定义服务暴露给客户端程序的操作,这些操作可供客户端程序接收和回传至服务端。你也已经了解到为什么要仔细地设计服务和数据协定,以及如何创建一个新版本的服务和数据协定的同时保持与现有客户端程序之间的兼容性。

【代码】 本章相关源代码下载

【题外】

谢谢热心同学的留言,从第七章开始,将根据各章内容的长短来调整文章的长短。确实文章太长了,你们看着累,我写着也累。并预祝大家端午节快乐:P

posted @ 2011-06-02 16:36  On the road....  阅读(2457)  评论(4编辑  收藏  举报