WCF4.0进阶系列—第八章 使用工作流实现服务
【前言】
企业使用WCF服务的一个主要原因是通过包装现有的组件和程序构建面向服务的应用,这些应用通过不仅简单而且适应力很强地方式重用。这种策略为企业带来了非常大的灵活性,因为它可以简单地响应快速变化的业务需求、并迅速地创建或更改系统以适应这些需求。许多企业采用的业务过程都由一系列明确的、并按照特定顺序执行的步骤组成。其中一些步骤可能涉及调用服务的操作,这需要确保WCF服务的操作顺序应当与基本业务过程相匹配。你已了解到在服务的方法上通过操作行为特性,可以指定某个操作发起或终止一个会话;除此之外,服务几乎再不能控制客户端调用服务操作的顺序。这增加了强制客户端程序按照顺序调用服务操作的困难,此外这种方式还可能导致难以发现(并纠正)的错误。使用工作流来定义服务可以帮助解决这个问题,并且强制客户端按照一定的顺序调用服务的操作。
另外一个潜在的问题是谁负责真正地定义和实现业务过程的逻辑。 没有谁比业务分析师能更好地掌握企业所采用业务过程。你不应该期望一个业务分析师同时还精通WCF、或者掌握如何实现WCF服务的操作;很明显这些任务应当是开发人员负责的。另外一个方面,开发人员可能非常擅长构建重用的组件和服务,但是不能充分理解企业所采用的业务过程。 WF(工作流)也可以帮助解决这个问题。业务分析师可以和开发人员一起工作,使用WF定义各种业务过程的图形模型,开发人员实现执行该模型描述的各种任务所要求的代码。
还有另外一个需要考虑的问题是服务的扩展性。 WF提供完美的框架实现长时间运行的业务过程。使用WF工作流构建的WCF服务可以将会话状态信息持久化。此外,在企业中,你可以使用Windows Server AppFabric寄宿和管理基于WF工作流的WCF服务。
在本章,你将看到使用WF工作流为业务过程建立模型,然后如何根据此模型构建WCF服务;此外,你还可以看到如何构建客户端程序与工作流服务互操作。
【正文】
构建一个简单的工作流服务和客户端程序
首先,我们将使用WF构建一个简单的服务,该服务暴露给外部调用的操作是无状态的。然后你将学习如何调从客户端调用该操作。
实现工作流服务
在本章的第一组练习中,让我们回顾ProductService服务,客户端程序访问该服务获取AdventureWorks销售的产品信息。作为工作流服务的一部分,你将实现GetProduction操作,该操作接受产品编号参数并返回产品的详细信息。
创建ProductsWorkflowService服务
1. 使用Visual Studio创建一个新的WCF工作流服务程序项目: 在创建新项目对话框中,选择Visual C#,然后选择Workflow,再选择WCF Workflow Service Application模板。在项目名称处输入ProductsWorkflowService; 然后选择一个你本地计算机的文件夹存储该项目;最后输入解决方案的名称ProductsWorkflow。
Visual Studio将在ProductsWorkflowService项目中创建一个的名为Service1.xamlx的工作流服务。下图显示了该服务:
这是一个简单的顺序化服务,它由WorkflowService活动构成,该活动包括一系列顺序活动定义的步骤。 ReceiveRequest活动等待由客户端指定的名为GetData操作的请求消息;SendResponse活动发送响应消息至客户端程序。目前,GetOperation操作不会执行其他流程;但是你将在ReceiveRequest活动和SendResponse活动之间添加必要的逻辑。
ReceiveRequest活动和SendResponse活动定义服务合约。在后面的步骤中,你将看到如果不想使用默认值该如何改变操作和服务合约的名字。
2. 在解决方案浏览器中,重命名Service1.xamlx为ProductsService.xamlx
3.在工作流的设计视图窗口中,点击顺序化服务活动边界外的背景。然后在属性窗口中,设置ConfigurationName和Name属性为ProductsService
工作流的ConfigurationName属性在服务配置文件web.config文件中用于指定该服务的名称。Name属性在服务元数据的WSDL描述中指定该服务的名字。
4. 在设计视图中点击ReceiveRequest活动。 在OperationName文本框中,输入GetProduct。这个名字就是在服务合约中该操作的名字。
5. 在属性窗口, 改变ServiceContractName属性值为{http://adventure-works.com}/IProductsService. 花括号内的内容为该服务合约的命名空间。
6. 确认ReceiveRequest活动仍然处于选中状态,然后点击设计视图窗口左下角的"变量"标签。修改变量data的名字为localProductNumber,并修改该变量的类型为String。GetProduct操作接受的消息包含由客户端程序提供的产品编号;并且该操作返回一个包含产品详细信息的消息。你将使用该变量保存客户端请求消息中的产品编号。
请不要修改或者删除handle变量。工作流使用该变量关联SendResponse活动和ReceiveRequest活动;当你完成上述操作后,再次点击"变量"标签,以隐藏变量定义。
注意:重命名data变量将导致该工作流的几个验证错误。这是因为ReceiveRequest活动和SendResponse活动仍旧引用data变量。你将在下一步中修改ReceiveRequest活动,并在下一个练习中更新SendResponse活动,使它们引用正确的变量并修正工作流的验证错误。
7. 在ReceiveRequest活动中,在"Content"属性中,点击"查看消息"。将显示内容定义对话窗口。你使用该窗口指定传递到该操作的参数。也可以通过消息对象或者参数列表指定该信息。消息对象是实现IMessage接口的类型。在第十一章你将了解到更多关于消息对象和IMessage接口的详细内容。
8. 在内容定义对话框中,选择参数选项,然后点击点击创建新参数。并使用下面的信息设置该参数:
属性
|
值
|
名称
|
ProductNumber
|
类型
|
String
|
分配至
|
localProductNumber
|
该操作的SOAP消息包含在此处定义的任何参数。 "分配至"字段的定义使工作流运行时通过特定的变量获取该参数的值,从而你可以在该工作流的其他活动中访问该参数。
注意:如果你不熟悉Windows workflow foundation,你可能会非常惊讶地发现"分配至"的值通过Visual Basic表达式来设定,尽管现在是你使用的是Visual C#。这不是一个错误。 当你设计一个工作流并且必须指定一个表达式作为活动定义的一部分, 你将始终使用Visual Basic语法。但是,当你编写代码支持活动和工作流时,你必须使用你创建工作流项目时指定的语言。
9. 在设计视图窗口中,点击ReceiveRequest活动,然后选择属性窗口,确认CanCreateInstance复选项被选中。该属性用以指定客户端程序调用该操作去创一个新的服务实例并开始新的会话。服务中只要有一个操作必须允许该属性。
现在,你已经为GetPorduct操作定义了请求消息。回忆之前的章节,该操作的目的是从AdventureWorks数据库中获取产品的详细信息,创建一个ProductData对象并为该对象赋值,最后返回ProductData对象至客户端。在本章中,我们同样需要实现上述功能,但是具体代码发生了变化。在接下里的练习中,你将看到这些具体的代码。
通过Visual Studio提供的标准工作流活动,可以实现GetProduct操作的基本逻辑。但是,WF并没有提供任何一个活动帮助你与数据库交互。因此,在下面的练习中,你将创建自定义的活动以查询AdventureWorks数据库,然后你将这些活动放加入定义GetProduct操作的工作流。自定义的活动包括
- ProductExists 该活动将测试特定产品编号的产品是否在AdventureWorks数据库中存在,并返回一个Boolean值。 你将使用实体框架连接到数据库,与之前章节一样仍旧需要使用ProductsEntityModel组件。 你将传递对象AdventureWorksEntities用以连接数据库,并传递产品编号作为该活动的输入参数。
- FindProduct 该活动将从AdventureWorks数据库中获取指定产品的详细信息,然后填充ProductData对象。在这之前,对于该活动的输入参数,你将传递AdventureWorksEntities对象连接数据库,同使还传入产品编号参数。该活动将返回填充数据后的ProductData对象。
创建ProductData类型,并实现ProductExists和FindProduct活动
1. 在Visual Studio中,添加如下文件到ProductsWorkflowService项目。
属性
|
值
|
模板
|
Code Activity
|
名称
|
ProductsService.Activities.cs
|
2. 添加引用ProductsEntityModel组件到该项目中
3. 添加引用System.Data.Entity组件和System.Runtime.Serialization组件
4. 打开文件ProductsService.Activities.cs,然后在该文件的头部添加下列using语句
using System.Runtime.Serialization;
using System.ServiceModel;
using ProductsEntityModel;
using System.Runtime.Serialization;
using System.ServiceModel;
using ProductsEntityModel;
5. 添加ProductData类到该文件
[DataContract]
public
class
ProductData
{
[DataMember]
public
string Name;
[DataMember]
public
string ProductNumber;
[DataMember]
public
string Color;
[DataMember]
public
decimal ListPrice;
}
6. 创建ProductsExists活动。所有代码活动都继承CodeActivity类。该类提供一个方法Execute,当代码活动被工作流调用时执行该方法。你使用活动运行时期望执行的代码来重写该方法。
public
sealed
class
ProductExists : CodeActivity<Boolean>
{
// Define an activity input argument of type string
public
InArgument<AdventureWorksEntities> Database { get; set; }
public
InArgument<string> ProductNumber { get; set; }
// If your activity returns a value, derive from CodeActivity<TResult>
// and return the value from the Execute method.
protected
override
bool Execute(CodeActivityContext context)
{
string productNumber = ProductNumber.Get(context);
AdventureWorksEntities database = Database.Get(context);
int numProducts = (from p in database.Products
where
string.Equals(p.ProductNumber, productNumber)
select p).Count();
return numProducts > 0;
}
}
7. 创建FindProduct活动
public
sealed
class
FindProduct : CodeActivity<ProductData>
{
public
InArgument<AdventureWorksEntities> Database { get; set; }
public
InArgument<string> ProductNumber { get; set; }
protected
override
ProductData Execute(CodeActivityContext context)
{
string productNumber = ProductNumber.Get(context);
AdventureWorksEntities database = Database.Get(context);
Product matchedProduct = database.Products.First(
p => string.Compare(p.ProductNumber, productNumber) == 0);
ProductData productData = new
ProductData()
{
Name = matchedProduct.Name,
ProductNumber = matchedProduct.ProductNumber,
Color = matchedProduct.Color,
ListPrice = matchedProduct.ListPrice
};
return productData;
}
}
8. 生成方案;此时你将得到错误消息"Compiler error(s) encountered processing expression data.ToString(). ToString is not a member of 'Data'". 这由SendResponse活动未通过验证的错误引起。此刻,先忽略该错误。我们将在后面的练习中纠正该错误。查看是否还有其他错误消息,我们必须确保在进行下一步之前没有其他的错误。
现在,你可以返回到定义GetProduct操作的工作流上, 实现其业务逻辑。 假设你的代码编译成功,你将在工具栏内发现已经自动添加FindProduct活动和ProductExists活动。
实现GetProduct操作的业务逻辑
1. 返回到ProductsService.xamlx的设计视图窗口,并在工具栏内确认FindProduct活动和ProductsExists活动已经添加。
2. 在设计视图窗口中,点击"顺序服务活动"以隐藏工具栏,然后点击设计视图窗口左下角的"变量"标签。添加下面的参数:
参数名
|
参数类型
|
范围
|
默认值
|
database
|
ProductsEntityModel.AdventureWorksEntities
|
顺序服务
|
New AdventureWorksEntities()
|
productData
|
ProductsWorkflowService.ProductData
|
顺序服务
|
Nothing
|
Exists
|
Boolean
|
顺序服务
|
False
|
当指定database变量的类型时,在变量类型下拉列表中,点击"浏览类型"。在"浏览和选择.NET类型"对话框中,在<Referenced assemblies>下,展开ProductsEntityModel[1.0.0.0],然后展开ProductsEntityModel,点击AdventureWorksEntities后,点击"确认"按钮。使用同样的方式,添加并设置变量productData的类型。
3. 在工具栏中,拖动ProductsExists活动到顺序服务中的ReceiveRequest活动与SendResponse活动之间:
4. 选中ProductsExists活动,在属性窗口中,设置其Database属性值为database,ProductNumber属性值为localProductNumber,并设置Result值为exists。
Database和ProductNumber属性是在ProductExists活动中定义的输入参数。localProductNumber变量包含来自客户端发送至ReceiveRequest活动的消息中的产品编号。database变量包含一个新的AdventureWorksEntities类型的新实例,该实例用以连接至数据库。result属性是Execute方法的执行的结果,并且该步骤将变量的值赋给exists变量。
Database和ProductNumber属性是在ProductExists活动中定义的输入参数。localProductNumber变量包含来自客户端发送至ReceiveRequest活动的消息中的产品编号。database变量包含一个新的AdventureWorksEntities类型的新实例,该实例用以连接至数据库。result属性是Execute方法的执行的结果,并且该步骤将变量的值赋给exists变量。
5. 在工具栏中,展开"Control Flow"区域,拖动If活动到顺序服务的ProductExists活动和SendResponse活动之间。在条件文本框内输入"exists"。
6. 在工具栏中,拖动FindProduct活动到If活动的Then框内;那么此时的工作流将如下如所示:
7.选中findProduct活动,在属性窗口中,设置Database属性值为database,ProductNumber属性值为localProductNumber,并设置Result属性为productData.
7.选中findProduct活动,在属性窗口中,设置Database属性值为database,ProductNumber属性值为localProductNumber,并设置Result属性为productData.
当exists变量值为true时,将调用FindProductData活动。Database和ProductNumber属性是FindProduct活动中定义的输入参数。Result属性是Productdata对象,其包含从AdventureWorks数据库中获取的产品的详细信息。如果exists变量值为false,那么变量productData将会被设置为Nothing(C#中为null)
8. 在设计视图窗口中,点击SendResponse活动,然后点击"查看消息"。在内容定义对话框中,选择参数选项。点击"浏览类型"以指定该参数的类型。点击"确认"按钮完成参数类型的指定。请按照下列规范设置该参数:
属性
|
值
|
名称
|
Product
|
类型
|
ProductsWorkflowService.ProductData
|
值
|
productData
|
上述设置指定SendResponse活动回传至客户端的消息。在本例中,该消息包含调用FindProduct活动获取的产品详细信息。
现在,你已经完成了GetProduct操作的定义。如果需要额外的功能,你还可以为该服务添加其他的操作,但是你需要记住该服务是一个工作流服务;它蕴含了客户端程序在什么时候,以什么方式调用该服务的操作(具体信息见本章后续内容)
到目前为止,该ProductsWorkflowService包含一个可以部署的、可之前访问其他服务的客户端访问的WCF服务。如果你检查web.config文件,你将发现该文件包含了所需的最基本的、且最少的配置信息,但通过这些信息,你可以获取该服务的元数据以构建相应的客户端。下图展示了web.config文件中ServiceModel片段:
如果你不希望使用默认的配置,你可以修改该配置文件,并定义服务端点。
如果你不希望使用默认的配置,你可以修改该配置文件,并定义服务端点。
下一步将是测试该服务是否能正常工作,并确认GetProduct操作能否返回正确的信息。最简单的方式是使用WCF默认的测试客户端程序。
测试ProductsWorkflowService服务
1. 打开ProductsWorkflowService项目下的web.config文件,添加下面的连接字符串到该文件:
<connectionStrings>
<add
name="AdventureWorksEntities"
connectionString="metadata=res://*/ProductsModel.csdl|res://*/ProductsModel.ssdl|res://*/ProductsModel.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=AdventureWorks;integrated security=False; user id=ap_wcf; password=ap_wcf; multipleactiveresultsets=True;App=EntityFramework""
providerName="System.Data.EntityClient" />
</connectionStrings>
该连接字符串供ProductsEntityModel组件使用以连接至AdventureWorks数据库
2. 保存web.config;然后回到ProductsService.xamlx工作流的设计视图窗口
3. 选择Visual studio的"调试"菜单,然后点击"开始调试"。
ASP.NET开发服务器启动,并作为该服务的宿主程序。WCF测试客户端程序同时启动并连接至该服务。WCF测试客户端程序查询该服务的元数据,并在左边面板中显示服务所提供的操作:
4. 在WCF测试客户端程序的左边面板中,双击GetProduct()
5. 在GetProduct面板中,面板的上部分为请求区域,在"值"字段下方的文本框中输入"WB-H098",然后点击"调用"。如果出现安全警告信息,点击"确认"按钮—因为你仅仅是在发送一个测试消息至运行在你计算机上服务,因此不存在安全问题。
6. 确认服务发送了包含WB-H098产品详细信息的响应。
7. 在WCF测试客户端工具中,点击右边面板下部的XML标签。该面板显示请求消息和响应消息的XML内容。你可以看到ProductData对象如何被序列化为响应消息的一部分。
8. 关闭WCF测试客户端工具。
实现工作流服务的客户端程序
客户端程序不需要知道服务是如何实现地,也不关心创建和寄宿服务使用的技术。重要的是开发人员构建客户端程序能访问服务元数据,知道使用哪个协议连接至服务。因此,你可以采用创建客户端程序访问其他类型WCF服务相同的方式,创建客户端程序以访问工作流服务。
创建工作流客户端程序
1. 在Visual Studio中,添加一个新项目到ProductsWorkflow解决方案中。 该项目使用的模板为工作流控制台程序模板。
2. 创建完项目后,重命名Workflow1.xaml文件为ClientWorkflow.xaml
3. 在设计视图窗口下打开ClientWorkflow.xaml;该工作流目前为空。你将添加活动到该工作流,以连接至ProductsWorkflowService并调用GetProduct操作。
4. 在属性窗口中,更改Name属性为ProductsWorkflowClient.ClientWorkflow
5. 打开program.cs文件;修改Main方法。将WorkflowInvoker.Invoke(new Workflow1());修改为WorkflowInvoker.Invoke(new
ClientWorkflow ());
6. 添加服务引用ProductsWorkflowService到ProductsWorkflowClient项目。(具体操作参见第二章);添加服务引用后,编译该项目。
7. 返回到ClientWorkflow工作流的设计视图窗口,并调出Visual Studio工具箱。
注意在工具栏顶部出现一个新的名为"ProductsWorkflowClient.ProductsWorkflowService.Activities"区域。该区域包含GetProduct代码活动。当你添加服务引用到工作流客户端程序时,服务引用添加向导为该服务的每一个对外的操作生成一个代码活动。你可以使用这些代码活动调用服务的操作。每个活动都作为一个代理,通过代理连接至服务,然后发送请求消息,并等待服务响应。
8. 在工具栏者中,展开"控制工作流"区域,拖动顺序活动到空的ClientWorkflow工作流上。
9. 拖动GetProduct活动至ClientWorkflow工作流的顺序活动中
10. 在属性窗口中,修改GetProduct活动的DisplayName属性值为Get Water Bottle
11. 检查该活动的其他属性。
EndpointConfiguration属性指定配置文件app.config中客户端端点的名字;工作流将使用该端点连接至服务。App.config文件是由添加服务引用向导自动生成,你可以编辑该文件,比如当服务更改了绑定地址,那么你需要重新配置客户端程序使用的绑定。
ProductNUmber属性对应ReceiveRequest活动为GetProduct操作所提供的参数。你应该使用调用操作前使用一个产品编码填充该属性。服务找到该产品的详细信息后,该信息被当作参数传输至SendResponse活动,然后SendResponse活动将ProductData对象回传至客户端
12. 在设计视图窗口中,点击Get Water Bottle活动,添加下面的参数:
参数名
|
参数类型
|
范围
|
默认值
|
productReturned
|
ProductsWorkflowClient.ProductsWorkflowService.ProductData
|
顺序服务
|
Nothing
|
13. 在设计视图模式中,选择Get Water Bottle活动,设置Name属性的值为productReturned;设置ProductNumber属性值为"WB-H098" (包括引号)
14. 在工具栏中,展开Primitives区域,拖动WriteLine活动之顺序活动中,并且职位处于Get Water Bottle活动之下。在Text文本框中,输入productReturned.Name:
15. 在WriteLine活动下面,再添加另外一个GetProduct活动到该工作流中,该活动包含下列属性:
属性
|
值
|
DisplayName
|
Get Mountain Seat Assembly
|
EndpointConfiguration
|
BasicHttpBinding_IProductsService
|
Product
|
productReturned
|
ProductNumber
|
"SA-M198"
|
16. 在设计视图中,复制WriteLine活动,然后在Get Mountain Seat Assembly活动下面粘贴该活动。下图显示了完成后的工作流:
17. 生成项目。
测试工作流客户端程序
1. 在项目浏览器中,在方案ProductsWorkflow上点击右键,然后点击"属性"
2. 设置多重启动项目。在属性页中,在左边面板中选择常用属性,然后选择项目启动,然后在右边面板中选择多个启动项目,然后设置两个项目的动作为start
3. 在非调适模式下,启动方案。ASP.NET开发服务器将启动,该服务器用以寄宿ProductsWorkflowService服务;客户端程序同时也将运行,并且在控制台窗口中显示产品WB-H098和产品SA-M198的全名。
4. 按ENTER键,关闭客户端程序并返回到Visual Studio
处理工作流服务的异常
在第三章"构建健壮的程序和服务"中,你已经看到如何在服务中捕获异常,并将这些异常报告给客户端程序。这是构建健壮系统重要的组成部分。让我们回顾一下基本步骤
1. 定义强类型的fault类,并标记DataContrat特性
2. 在每个服务操作的逻辑中,捕获可能发生的任何异常
3. 在异常处理器中,确定异常的原因,构建对应的fault类的实例,然后填充响应的信息到该实例上
4. 抛出类型安全的FaultException<>异常,该异常能接收fault对象。
你可以在工作流服务中应用同样的逻辑。在工作流服务中捕获异常,并将这些异常作为类型安全的faults报告给客户端的方式与常规的WCF服务的实现方式非常相似。使用工作流服务的问题是如何发送FaultException<>至客户端程序。
在使用C#构建的常规WCF服务中,你可以在一个操作上标记FaultContract特性以指定该操作可能生成的faults。在该操作定义的内部,你可以简单地抛出一个FaultException<>异常,WCF运行时将完成剩下的一切任务:WCF运行时创建一个fault消息,并为该消息填充FaultException<>指定的数据,然后将该消息作为服务响应回传至客户端程序。当你使用工作流服务,该服务的操作从为Receive活动指定的属性中派生。你可以指定这些项目,比如操作名、服务合约名、操作能接收的请求消息模型,但是你不能采用FaultContract特性。相应地,为了能在工作流服务的操作中抛出FaultException<>异常,你需要执行显示地执行一些任务(这些任务构成工作流的一部分)。特别地,你必须在工作流中合适的位置添加额外的SendReply活动,然后配置它们,以使其能发送可能出现的各种fault类型。
在下面的练习中,你将重新实现第三章中的SystemFault和DatabaseFault类。你还将修改GetProduct操作以捕获异常。如果异常的原因是数据库问题,该操作将抛出一个DatabaseFault;否则,将抛出一个SystemFault。
为ProductsWorkflowService服务添加错误处理
准备工作:在*\WCF\Step.by.Step\Chapter8文件夹下建立ProductsWorkflowFaultHandling文件夹,然后 复制ProductsWorkflow文件夹内的ProductsWorkflowService到新建文件夹。
1. 打开ProductsWorkflowFaultHandling文件夹中ProductsWorkflowService下的ProductsWorkflowService.csproj
2. 打开ProductsService.Activitieis.cs文件,并添加两个数据合约DatabaseFault和SystemFault
[DataContract]
public
class
SystemFault
{
[DataMember]
public
string SystemOperation { get; set; }
[DataMember]
public
string SystemReason { get; set; }
[DataMember]
public
string SystemMessage { get; set; }
}
[DataContract]
public
class
DatabaseFault
{
[DataMember]
public
string DbOperation { get; set; }
[DataMember]
public
string DbReason { get; set; }
[DataMember]
public
string DbMessage { get; set; }
}
|
3. 生成项目;该步骤很重要,因为如果不执行该步骤,上述两个新的类型将不能在工作流中被识别。
4. 在设计视图窗口下打开ProductsService.xamlx,然后在工具栏中,展开错误处理区域,拖动TryCatch活动到顺序服务中,并且其位置处于ReceiveRequest活动与ProductsExists活动之间。
5. 在工具栏中,从控制流区域中拖动一个顺序活动到TryCatch活动的Try框内
6. 在设计视图窗口中,拖动ProductExist、if、和SendResponse活动及其它们的内容到TryCatch活动try框内的顺序服务中。完成后,工作流将如下图所示:
7. 在TryCatch活动的Catches区域内,点击"添加新异常"。将出现异常类型的下拉列表,选择System.Exeception,然后按Enter键。Catches区域将展开,并显示为一个区域,在该区域内你可以添加处理异常的活动。
8. 从工具栏的控制流区域中选择添加一个if活动,并将该活动拖动至上述步骤创建异常活动区域中。在条件框内输入TypeOf exeception.InnerException Is System.Data.SqlClient.SqlException. 该表达式用以检查抛出的异常是否为SqlException
9. 点击"变量"标签,然后添加下面的参数:
参数名
|
参数类型
|
范围
|
默认值
|
dbf
|
ProductsWorkflowService.DatabaseFault
|
TryCatch
|
Nothing
|
10. 添加一个顺序活动到异常处理器的If活动下Then区域内。
11. 添加一个Assign活动到上述新创建的顺序活动中。在该活动的To文本框内,输入dbf。然后打开属性窗口,并点击Value属性后的省略符按钮。在表达式编辑对话框中,输入下列表达式
New DatabaseFault() With { .DbOperation = "Connect to database", .DbReason="Exception accessing database", .DbMessage=exception.InnerException.Message }。该表达式使用VB代码创建一个新的DatabaseFault对象,并且将引发错误对应的异常填充到该对象中。然后将该对象传送至客户端。你将使用SendReply活动发送响应,但在工具箱中不能发现该活动。你必须在Visual Studio中通过Receie活动自动生成一个配置好的SendReply活动。
New DatabaseFault() With { .DbOperation = "Connect to database", .DbReason="Exception accessing database", .DbMessage=exception.InnerException.Message }。该表达式使用VB代码创建一个新的DatabaseFault对象,并且将引发错误对应的异常填充到该对象中。然后将该对象传送至客户端。你将使用SendReply活动发送响应,但在工具箱中不能发现该活动。你必须在Visual Studio中通过Receie活动自动生成一个配置好的SendReply活动。
12. 在设计视图窗口中,找到ReceiveRequest活动,然后在该活动上点击右键,然后点击创建SendReply。设计器将添加一个名为SendReplyToReceiveRequest的SendReply活动,该活动位置ReceiveRequest活动的下方。拖动SendReplyToReceiveRequest活动到Catches活动下àIf活动中à顺序服务àAssign活动的下方。然后更改其DisplayName为Send DatabaseFault
13. 在Send DatabaseFault活动中,点击内容定义框。 内容定义对话框将出现。选择参数选项,然后添加下面的参数:
参数名
|
参数类型
|
值
|
databaseFaultException
|
System.ServiceModel.FaultException<ProductsWorkflowService.DatabaseFault>
|
New FaultException(Of DatabaseFault)(dbf)
|
14. 点击"变量"标签,然后添加下面的参数
参数名
|
参数类型
|
范围
|
默认值
|
sf
|
ProductsWorkflowService.SystemFault
|
TryCatch
|
Nothing
|
15. 添加一个顺序活动到异常处理器的Else区域中,然后添加一个Assign活动到新添加的顺序活动中。 在Assign活动的To文本框内,输入sf。然后打开属性窗口,点击Value属性后的省略符按钮。在出现的表达式编辑对话框中,输入表达式:New SystemFault() With { .SystemOperation = "Cet Product", .SystemReason="Exception finding product details", .SystemMessage=exception.Message }。该表达式使用VB代码创建一个新的SystemFault对象,并且将引发错误对应异常的值填充到该对象中,然后传递给客户端。
16. 在设计视图窗口中,找到ReceiveRequest活动,然后点击右键,再次点击"创建SendReply"。 SendReply创建之后,拖动SendReplyToReceiveRequest活动至Catches活动下àelse活动中à顺序服务àAssign活动的下方。然后在属性窗口中,更改该活动的DisplayName为Send SystemFault
17. 在Send SystemFault活动中,点击内容定义框。内容定义对话框将出现。选择参数选项,然后添加下面的参数:
参数名
|
参数类型
|
值
|
systemFaultException
|
System.ServiceModel.FaultException<ProductsWorkflowService.SystemFault>
|
New FaultException(Of SystemFault)(sf)
|
18. 重新生成ProductsWorkflowService项目。
你可以快速的检查服务的元数据以确认服务是否正确的实现错误处理。若要这么做,那么你在ProductsWorkflowService项目上点击右键,然后选择"在游览器中查看"。IE将自动启动,并显示ProductsService服务页。在该页中,你点击http://localhost:42969/ProductsService.xamlx?wsdl (注意:端口号可能不同)。然后你将看到该服务的WSDL描述,从该文件中,我们可以发现DatabaseFault和SystemFault消息已经在GetProduct方法中生成。
现在,你可以开始测试ProductsWorkflowService服务的错误处理能力。要测试错误处理,你需要在客户端程序中添加捕获FaultException<DatabaseFault>和FaultException<SystemFault>异常。因为一些人的个人偏好,也为了证明工作流服务可以和非工作流客户端程序一起工作,我们将使用第三章所创建的客户端项目。
测试ProductsWorkflowService服务的的错误处理
准备工作:复制第三章的ProductsClient项目到*\WCF\Step.by.Step\Chapter8\ProductsWorkflowFaultHandling文件夹下
1. 在Visual Studio中,添加ProductsClient项目;
2. 为ProductsClient项目添加服务引用;在添加服务引用对话框中,点击"识别服务";然后在Namespace文本框处,输入ProductsService,然后点击确定
3. 修改Programm.cs
static
void Main(string[] args)
{
Console.WriteLine("Press ENTER when the servcie has started");
Console.ReadLine();
ProductsServiceClient proxy = new
ProductsServiceClient("BasicHttpBinding_IProductsService");
try
{
Console.WriteLine("Display the details of a product");
string procutNumber = "WB-H098";
ProductData product = proxy.GetProduct(procutNumber);
Console.WriteLine("Name:" + product.Name);
Console.WriteLine("Color:" + product.Color);
Console.WriteLine("Price:" + product.ListPrice.ToString());
}
catch (FaultException<DatabaseFault> dbf)
{
Console.WriteLine("DatabaseFault {0}: {1}\n{2}",
dbf.Detail.DbOperation,
dbf.Detail.DbMessage,
dbf.Detail.DbReason);
}
catch (FaultException<SystemFault> sf)
{
Console.WriteLine("SystemFault {0}: {1}\n{2}",
sf.Detail.SystemOperation,
sf.Detail.SystemMessage,
sf.Detail.SystemReason);
}
catch (FaultException ex)
{
Console.WriteLine("{0}: {1}", ex.Code.Name, ex.Reason);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}
|
4. 在方案属性中,设置ProductsWorkflowService和ProductsClient均为启动项目
5. 在非调适模式下启动方案。在客户端控制台应用程序中,按ENTER键以连接至服务,然后确认Water Bottle产品的详细信息显示在控制台窗口中。再次按下ENTER键,以关闭客户端控制台窗口。
6. 在ProductsWorkflowService,将链接字符中的user id=ap_wcf修改为user id=ap_wcf1
7. 生成方案然后再次在非调适模式下运行方案。在客户端控制台程序窗口中,按下ENTER键以连接池服务。但是这次,服务将返回一个FaultException<DatabaseFault>消息;其具体信息如下图所示:
8. 关闭客户端控制台窗口,并返回到Visual Studio
9. 在ProductsWorkflowService中,恢复连接字符串中的user id.
寄宿工作流服务
到现在为止,都是使用Visual Studio自带的ASP.NET开发服务器寄宿工作流服务。此外,你还可以在其他环境中 寄宿WCF服务,比如IIS/WAS或者自定义一个宿主程序。
在IIS中寄宿工作流服务
在IIS中寄宿工作流服务与寄宿常规服务非常相似。但有一点不一样;在第一章的使用IIS寄宿WCF服务时,你使用发布站点向导;而寄宿工作流服务,你需要使用创建部署包向导。
部署ProductsWorkflowService到IIS
准备工作:在*\WCF\Step.by.Step\Chapter8文件夹下创建HostProductsWorkflowService文件夹;然后进入该文件夹创建IIS和Custom文件夹;
1. 在Visual Studio中,在ProductsWorkflowFaultHandling方案下的ProductsWorkflowService项目上点击右键。然后点击"打包/发布设置"
2. 设置包的位置和IIS中application的名字:
3. 保存设置后,再次在项目上点击右键,然后点击"生成部署包",等待"发布成功"消息出现在Visual Studio的状态栏中
4. 在资源管理器中,定位到*\WCF\Step.by.Step\Chapter8\HostProductsWorkflowService\IIS目录,确认ProductsWorkflowService.zip文件已经创建。
该文件包含服务部署包。你可以复制该文件到运行IIS的服务器上,然后在服务器上安装该包。在本章的练习中,IIS运行在同一台计算机上,因为你不需要复制文件的过程。
5. 使用管理员身份运行IIS
6. 在IIS左边的连接面板中,展开计算机节点,然后展开站点,最后点击默认的web站点
7. 在IIS右边的行为面板中,点击导入程序。将启动导入程序包向导。
如果你没有看到导入程序链接,请先安装Web部署工具。http://www.iis.net/download/WebDeploy
如果你没有看到导入程序链接,请先安装Web部署工具。http://www.iis.net/download/WebDeploy
8. 当导入程序包向导启动后,点击浏览按钮,选择第三步生成的文件ProductsWorkflowService.zip。然后点击下一步按钮
9. 在安装包内容页面,确认所有内容都被选中。然后点击"下一步"
10. 在输入应用程序安装包信息页。确认应用程序路径和链接字符串。然后点击"下一步"
11. 等待服务被安装完毕。在安装过程和结果页面,确认结果的提示信息"两个目录和五个文件已经添加"。然后点击"结束"按钮
12. 在IIS管理中,展开默认web站点,你将看到ProductsWorkflowService程序已经列出。
13. 点击ProductsWorkflowService程序,然后点击内容试图标签。你将发现三个项目:bin文件夹,ProductsService.xamlx文件和web.config文件
14. 在IIS左边的连接面板,选择ProductsWorkflowService程序,然后点击右键,选择管理应用程序,并选择高级设置。在高级设置对话框中,确认任应用程序池使用ASP.NET v4.0,然后点击确认按钮。
15. 在内容试图面板中,选择ProductsService.xamlx文件,然后点击右键,选择在浏览器中查看。IE将自动启动,并打开ProductsWorkflowService服务页。点击链接http://localhost/ProductsWorkflowService/ProductsService.xamlx?wsdl可以查看该服务的元数据。
16. 关闭IE浏览器,并返回到Visual Studio
通过配置客户端程序连接到寄宿在IIS的服务,你可以测试该服务的功能是否正确
测试寄宿在IIS中的ProductsWorkflowService服务
准备工作:复制ProductsWorkflowFaultHandling文件夹下的ProductsClient项目到HostProductsWorkflowService文件夹下。
1. 使用Vsisual Studio打开ProductsClient项目,然后打开该项目下的web.config文件
2. 修改端点的地址为 http://localhost/ProductsWorkflowService/ProductsService.xamlx
3. 在Visual Studio的方案浏览器中,在ProductsClient上点击右键,然后点击"设置为启动项目";然后在非调适模式下启动方案
4. 在客户端控制台程序中,点击ENTER键连接到服务。确认客户端程序成功地连接至服务,并且返回water bottole的详细信息
5. 关闭客户端控制台窗口,然后返回到Visual Studio
在自定义程序中寄宿工作流服务
寄宿工作流服务与寄宿非工作流服务很相似,但并不完全一样。最主要的区别在与寄宿程序必须提供创建和管理工作流的运行时。幸运的是,.net framework提供了WorkflowServiceHost类以支持上诉的需求。WorkflowServiceHost 位于WorkflowServiceHost.Actitivities命名空间下,因此如果你使用该类,必须引用WorkflowServiceHost.Activities命名空间。
注意:相当令人困惑的是,在.NET类库中,有两个WorkflowServiceHost类:
它们一个位于System.ServiceModel.Activities;另一个位于System.ServiceModel。后者是为NET Framework3.0构建,它实现了不同的工作流模型。如果你使用.net framework4.0构建寄宿工作流服务的应用充许,你需要使用前者命名空间中的WorkflowServiceHost类
它们一个位于System.ServiceModel.Activities;另一个位于System.ServiceModel。后者是为NET Framework3.0构建,它实现了不同的工作流模型。如果你使用.net framework4.0构建寄宿工作流服务的应用充许,你需要使用前者命名空间中的WorkflowServiceHost类
WorkflowServiceHost类一个与ServiceHost类似,其提供类似的方法,属性,和事件;当寄宿服务时的设定也仅仅只有一两处的不同。最值得注意的不同点是WorkflowServiceHost类提供了一个构造器,该构造器可以接受一个定义工作流服务根节点活动并当宿主程序接收到请求时启动该活动。下面两张图也说明了这点。
另外一个有用的构造器,接受WorkflowService对象。如下图所示,在后面的练习中我们将使用该构造器。
创建一个自定义程序寄宿ProductsWorkflowService服务
1. 在*\WCF\Step.by.Step\Chapter8\HostProductsWorkflowService\Custom文件夹下创建一个项目名为ProductsWorkflowHost的工作流控制台应用程序。创建完项目后,删除Workflow1.xaml
2. 将ProductsWorkflowFaultHandling文件夹下的ProductsWorkflowService项目添加添至方案ProductsWorkflowHost。
3. 添加引用ProductsWorkflowService到项目ProductsWorkflowHost
4. 打开ProductsWorkflowHost项目下的Program.cs文件,在文件头部,添加下面的using语句
using System.ServiceModel.Activities;
using System.Xaml;
using System.ServiceModel.Activities;
using System.Xaml;
5. 删除Main方法中现有的代码;并添加下面的代码
static
void Main(string[] args)
{
WorkflowService service = XamlServices.Load(@"..\..\..\..\..\ProductsWorkflowFaultHandling\ProductsWorkflowService\ProductsService.xamlx") as
WorkflowService;
WorkflowServiceHost host = new
WorkflowServiceHost(service);
host.Open();
Console.WriteLine("Service is running. Press ENTER to stop");
Console.ReadLine();
host.Close();
}
|
第一行基于ProductsWorkflowService项目下的ProductsService.xamlx文件创建WorkflowService对象。 该文件包含了ProductsWorkflowService服务的定义。 XamlService类的静态Load方法可以读取任何包含描述工作流的文件;并将它转化成图形对象。ProductsService.xamlx文件包含一个工作流服务,因为它可以安全地转换成一个WorkflowService对象。
第二行声明创建一个WorkflowServiceHost对象;该对象用于寄宿工作流服务。和ServiceHost对象一样,WorkflowServiceHost类允许你使用代码配置服务的端点、或者从配置文件中读取配置信息。在此练习中,你将使用配置文件中的配置。
剩下的代码你应该很熟悉。WorkflowServiceHost的Open方法使宿主程序开始侦听请求;close方法用以停止服务。
6. 使用WCF服务配置工具打开ProductsWorkflowHost配置文件app.config
7. 在配置面板中,点击服务端点,然后点击创建一个新服务链接启动创建服务元素向导,并创建一个新的服务端点。使用下表中的值指定向导中对应的属性。
页面
|
提示框
|
值
|
指定服务类型
|
服务类型
|
ProductsService
|
指定服务合约
|
服务合约
|
IProductsService
|
指定通讯模式
|
TCP
|
|
指定端点地址
|
地址
|
Net.tcp://localhost:8080/ProductsService.xamlx
|
8. 保存配置文件,并退出WCF服务配置工具
9. 使用Visual Studio打开配置文件,然后添加连接至AdventureWorks数据库的连接字符串
<connectionStrings>
<connectionStrings>
<add
name="AdventureWorksEntities"
connectionString="metadata=res://*/ProductsModel.csdl|res://*/ProductsModel.ssdl|res://*/ProductsModel.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=AdventureWorks;integrated security=False; user id=ap_wcf; password=ap_wcf; multipleactiveresultsets=True;App=EntityFramework""
providerName="System.Data.EntityClient" />
</connectionStrings>
10.保存app.config
测试寄宿的工作流服务
1. 添加ProductsClient项目到方案ProductsWorkflowHost中;然后打开ProductsClient的app.config文件。并添加一个新的客户端端点
<endpoint address="net.tcp://localhost:8080/ProductsService.xamlx" binding="netTcpBinding" contract="ProductsService.IProductsService" name="NetTcpBinding_IProductsService" />
<endpoint address="net.tcp://localhost:8080/ProductsService.xamlx" binding="netTcpBinding" contract="ProductsService.IProductsService" name="NetTcpBinding_IProductsService" />
2. 修改Program.cs文件,使客户端代理通过新建立的客户端端点连接至服务
3. 在方案ProductsWorkflowHost的属性中,设置ProductsClient项目与ProductsWorkflowHost项目为启动项目;并确认ProductsWorkflowService未非启动项目。
4. 在非调适模式下,运行方案。 如果Windows安全警告出现,点击"允许"按钮以使ProductsServiceHost程序打开TCP的8080端口
5. 在客户端程序控制台窗口中,按ENTER键连接至ProductsWorkflowService服务。确认客户端程序正常工作,并成功连接至服务而且获取到产品的详细信息
6. 关闭客户端控制台窗口,关闭寄宿服务的控制窗口。并返回到Visual Studio
在工作流服务中实现常用的消息模式
你已经看到你可以创建工作流服务,而且该工作流服务循环地执行"等待请求,发送响应"的消息处理过程。但是,这仅仅是客户端程序和服务通常使用的消息模式之一。该模式下服务的操作,客户端关注的是,是否存在一个同步线程控制从客户端传递至服务,并从服务端返回;当客户端程序发送请求至服务,看起来好像是在调用本地一个方法,并且直到服务的响应达到客户端之前,都不会消耗任何处理过程。 在第十二章,你可以看到使用其他模式(非消息模式)创建并实现WCF客户端程序和服务。比如,一个客户端程序能发送一个one-way消息至服务,然后立即执行处理过程。
异步消息模式下,一个客户端程序发送请求后,当服务开始处理请求时,客户端执行运行过程。如果服务需要发送回应,客户端程序需要在另外一个单独的线程中配置侦听服务的响应;并在接受到响应后开始处理该响应。
另外一种常见的消息模式是回调。在该模式下,服务能调用一个客户端程序,并且尽可能警告客户端程序服务的状态发生改变。在该模式下,来自客户端程序的一个单独的请求消息可以打开到服务的一个通道,服务使用该通道把消息回传至客户端,甚至发送请求消息以获取客户端的响应。在第16章你将学习到具体的细节。
WCF提供了特性和属性,你可以使用他们配置服务合约,操作合约,服务,和客户端程序,当然你也可以使用代码实现去实现这些配置。 如果你使用工作流实现服务和客户端程序,你可以使用消息活动实现上述及其他的消息模式。
消息活动
你应该已经注意到工作流工具箱有一个区域名为Messaging。该区域内的活动为工作流服务发送,接受,和关联消息。下表总结了其中一些行为的目的。如果你需要更详细的信息,请查阅MSDN或者Visual Studio帮助文档
属性
|
值
|
Receive
|
该活动封装了在端点侦听消息和等待消息。你可以通过设定Content属性来指定消息信息形式;并将该信息赋予给工作流中的变量。你还可以指定操作的名字及服务合约的名字,以便工作流运行时使用获取服务合约。
另外一个重要的属性是CanCreateInstance。如果该属性设置为true,该活动指定类型的消息可以启动一个新的服务实例并对该客户端创建一个新的会话(如果当前没有运行的客户端)。 如果该属性值设置为false,在服务接受并处理消息之前该客户但的会话将一直存在。 如果你希望实现消息级别的安全,你可以使用ProtectionLevel属性为签名并加密消息 |
SendReply
|
该活动不会出现在Visual Studio的工具箱中,尽管你经常使用该活动。该活动的目的是向客户端发送服务的响应。工作流中的每个发送响应活动应该有一个对应的接收活动, 该接收活动上点击右键并选择创建发送响应活动,可以生成一个配置好的发送响应活动
|
ReceiveAndSendReply
|
这是一个复合活动,它由接收活动和发送响应活动构成。Visual Studio使用WCF工作流服应用程序模板生成单个接收并发送响应活动,你可以使用它定义一个操作的起始点
|
Send
|
一个客户端 可以使用发送活动向服务发送一个请求消息。和接收活动一样,通过Content属性指定发送消息的形式。你通过设置Endpoint、EndpointAddress和EndpointConfiguration属性设置发送消息目的端点(服务端点)和发送消息端点(客户端端点)的详细信息。此外,你还应指定操作名和服务合约名以识别在服务中调用的操作。
如果服务实现了消息级别的安全,你需要在发送活动中设置ProtectionLevel属性使其与服务端的接收活动该属性的值匹配。 |
ReceiveReply
|
与发送响应相似,你不能在工具栏中发现该活动。当客户端程序使用发送活动发送请求消息,客户端会提供一个对应的接收回应活动以获取服务的响应消息。在发送活动上点击右键并选择创建接收响应活动,可以生成一个配置好的接收响应活动
|
SendAndReceiveReply
|
与接收并发送响应一样,该活动也是一个复合活动,它由发送活动和对应的接收响应活动构成。
|
请注意,在本章前面的练习中你所创建的工作流客户端程序,你并未显示地使用发送或者发送并接收响应活动。相反,你使用了自定义的GetProduct活动,该活动是由添加服务引用向导自动生成。实际上,GetProduct活动是一个简单的复合活动,该活动包含了一个发送活动、接收响应活动及赋值活动嵌套组成的顺序互动(如下图所示)。
活动向服务发送GetProduct消息并等待服务响应。由服务通过响应消息的回传值被赋值到一个临时变量, 该变量就是自定义的GetProduct活动的返回值。
在工作流服务实例中关联请求和回应消息
之前章节展示了服务的宿主程序可以支持服务的多个实例。当一个客户端程序连接到服务实例,客户端创建一个通道并使用该通道向服务实例发送请求消息,服务端的WCF运行时确保响应通过正确的通道发送至正确的客户端。当你向工作流服务发送消息时,接收活动为请求创建一个标识符,并确保发送响应的活动使用该标识符将对应的响应通过正确的通道发送至客户端程序。该标识符被命名为correlation handle。你可以在接收活动的CorrelationInitializers属性中使用这个correlation handle。当你为接收活动创建一个发送响应活动,WCF设计器为你自动创建一个CorrelationHandle变量,并使用接收活动的Correlation Initializer属性的值填充该变量。这些变量一般都命名为_handle1, _handler2,等等。你不需要理解correlation handler在内部是如何工作或者包含的数据,只需要知道它们用以区分请求并确保响应被正确的回传至对应的客户端。
当你在工作流中添加发送活动并创建一个接收响应活动时,上述逻辑同样适用。发送活动的Correlation Initializer属性自动填充一个correlation handle,然后接收响应活动也关联了相同的correlation handle
使用消息行为实现消息模式
一个服务最常见的消息模式是接收请求/发送响应消息循环,你可以使用接收并发送响应复合行为实现该模式。你可以使用操作的名字、服务合约和消息类型简单地配置接收活动。当一个工作流处于接收活动状态时,工作流将被锁定直到接收到匹配的消息。你可以提供必要的逻辑以处理接收到的消息,并创建响应,该响应通过发送响应活动回传至客户端。请注意,发送响应活动使用与接收消息相同的通道。如果服务发现异常,你可以配置额外的发送响应活动以发送FaultException消息至客户端。
在这种模式下,客户端程序使用发送并接收响应复合动作,该活动通常由添加服务引用向导生成的代理活动中,并且该活动已经被预先配置好。发送并接受响应活动减少服务的行为;发送活动向服务发送消息,然后执行接收响应活动,在这期间处于锁定状态,直到从发送消息的通道中接收到服务端的响应。与接收并发送响应活动利用服务一样,发送和接受响应活动包含在顺序活动中,因此在发送请求后并在接收到响应之前插入额外的逻辑。然而,你应该尽量保持任何处理过程尽量短暂,因为当服务发送响应时如果一个客户端没有执行接收响应,服务将被锁定。
你可以在客户端程序中使用发送活动实现一个简单形式的one-way消息,并且不用等待服务响应。该服务应使用接收活动以接受消息,但必须不可以试图发送响应;否则,该服务将被锁定,也可能不确定—这包含发送任何错误消息。
基本的异步消息模式要求一点额外的配置。客户端程序能使用发送活动以向服务提交一个请求,然后开始执行其要求的任何过程。但是,客户端程序必须准备接收该服务的响应,并且不能不适当地锁定服务。 一个可以实现该目标的较好方式是在客户端使用Parallel活动;当客户端处理服务的一个响应消息时,该活动中其他的请求消息可以顺序地等待服务的响应。
在异步模型中,服务在接收活动上等待以侦听请求,然后生成一个响应消息。但是,没有什么能阻止服务生成多个回应。在客户端程序向经纪服务发送消息询问产品报价的场景下。服务可能向多个提供产品报价服务发送请求。当服务接收到从各个供应商处得到响应后,该服务向客户端发送对应的响应。如果多个服务提供商向服务发送了响应,那么服务可能向客户端发送多个响应。在该模式下,客户端必须准备好接收多个响应。你可以在客户端工作流中使用While活动以达到该目的:侦听服务的响应,依次地接收并处理每个响应消息。
管理会话和维护工作流服务的状态
到目前为止你在本章中研究的工作流服务都只有比较简单的特性,暴露给客户端简单的都是无状态的操作。在现实世界中,工作流服务是相当复杂的。它们经常需要维护会话状态,并提供多个操作。当你使用程序实现WCF服务时,你可以指定服务的会话的模式、使用方法去实现服务的操作。WCF运行时管理服务实例,操作可以按照任何顺序实现,因为方法的顺序在C#类中并不重要。
但是,这对于工作流服务来讲发生了变化。工作流的重点在于为各种不同的任务定义它们的操作顺序。你可以通过在每个操作上使用接收和发送响应活动实现工作流服务,但是,当你需要实现多个操作,并侦听不同的请求消息,你该如何为设置这些活动顺序? 此外,你应该如何维持服务实例及其它的状态信息? 比如,回想第七章中的购物车服务。 该购物车服务提供了四个操作:添加商品、移除商品、获取购物车的信息和执行结算。该服务的业务规则指定一个客户端程序必须调用添加商品为第一个请求并初始化一个会话,创建服务实例,然后实例化会话状态;结算操作终止该会话,并销毁状态信息。除此之外,客户端还可以按照任何顺序调用添加商品、移除商品和获取购物车操作。如果你考虑WCF运行时如何处理这种情况,你可以看到WCF运行时的本质:执行一些循环,等待请求消息,分配这些请求至适当的方法。为了在工作流服务中提供相同的功能,你可以使用While和Pick活动实现相同的逻辑,这就是你在下面的练习中需要做的事情。
创建ShoppingCartService工作流服务
1. 启动Visual Studio使用WCF工作流服务程序模板创建一个新的解决方案。按照下表内容指定该方案的属性
属性
|
值
|
项目名称
|
ShoppingCartService
|
位置
|
*\WCF\Step.by.Step\Solutions\Chapter8
|
方案名称
|
ShoppingCart
|
2. 在解决方案浏览器中,重命名Service1.xamlx文件为ShoppingCartService.xamlx
3. 在设计视图窗口中,点击顺序服务边框外的背景;然后在属性窗口中,设置ConfigurationName和Name属性的值均为ShoppingCartService
4. 引用*\WCF\Step.by.Step\Solutions\Chapter8文件夹下的组件ProductsEntityModel到该项目;并添加System.Data.Entity系统组件
5. 添加ShoppingCartService.Activities.cs文件到ShoppingCartService项目,该文件位于*\WCF\Step.by.Step\Solutions\Chapter8文件夹下
该文件包含下面三个子项:
- ShoppingCartItem类,该类与第七章中ShoppingItemCart类的相同。它定义了购物车架构。每个会话将创建其该类的自己专属的实例,并且它将在客户端发出的请求之间维持会话中购物车的状态。
- FindItem活动,该活动实现find实用方法,find方法用于检查一个购物车以确定购物车是否已经包含一个指定产品编码的商品。购物车和产品编码由该活动的输入参数指定。该活动返回该购物车中商品的引用地址;如果商品为发现那么将返回一个空引用地址。
- GetItemFromDatase代码活动,使用实体框架从数据库获取特定产品的详细信息。连接至数据库的实体上下文和产品编码是由输入参数提供。该活动创建一个产品对象并返回一个从数据库里找到匹配的产品,否则返回空值。
6. 生成方案
7. 返回显示ShoppingCartService.xamlx的设计视图窗口,删除顺序化服务及其内容。工作流现在是空的
8. 从工具栏的控制流区域,添加一个顺序活动到该工作流。
9. 添加一个While活动到上一步添加的顺序活动中
10. 使用变量标签,添加一个名为serviceRunning的Boolean变量至该工作流。设置该参数的范围为While活动,并设置其默认值为True
11. 在While活动中,色黄志条件表达式为serviceRunning
你将添加侦听请求消息的活动至While活动。如果serviceRunning的值为false,那么While活动将中止,并且服务实例也将关闭。
你将添加侦听请求消息的活动至While活动。如果serviceRunning的值为false,那么While活动将中止,并且服务实例也将关闭。
12. 在设计视图窗口点击导入标签。在输入或选择命名空间框中,输入ShoppingCartService,然后按ENTER键
在文件ShoppingCartService.Activities.cs的ShoppingCartService命名空间中,定义了ShoppingCartItem类,FindItem活动及GetItemFromDatabase代码活动。使用导入标签,其功能和C#代码的语句一样的功能,你可以将命名空间引入到当前的范围。
13. 点击变量标签,添加另外一个名为shoppingCart变量。该变量的类型为System.Collection.Generic.List<ShoppingCartService.ShoppingCartItem>。设置该变量的范围为While活动,默认值为New List(Of ShoppingCartItem)()
14. 从工具栏的控制流区域,选择Pick活动添加至While活动内。
Pick活动对于创建需要响应任何顺序事件的工作流特别有用。该活动包含一个或多个PickBranch活动,一个PickBranch有两个元素:一个触发器和一个行为。在触发器区域,你需要指定一个等待事件的行为,比如等待即将到来的消息。在行为区域,你需要指定当事件发生时将运行的工作流。当一个工作流达到Pick活动,工作流将中止直到特定的事件发生,然后工作流执行对应的行为。
15. 点击PickBranch活动的Branch1,然后在属性窗口中更改显示名为Add Item to Cart
16. 点击变量标签,添加一个名为productNumber的字符串类型变量,其默认是为Nothing,并设置该变量的范围是Add Item to Cart活动
17. 在工具栏消息区域中,选择Receive活动并添加至Add Item to Cart活动的Trigger区域。并按照下表的内容指定该活动的属性
属性
|
值
|
DisplayName
|
Receive AddItemToCart Request
|
CanCreateInstance
|
选中该复选框
|
OperationName
|
AddItemToCart
|
ServiceContractName
|
{http://adventure-works.com/}IShoppingCartService
|
注意:CanCreateInstance属性指定消息是否可以用来创建服务的新实例并创建一个新会话。
18. 选则Receive活动,在属性窗口中,点击Content属性后的省略号按钮。在内容定义对话框中,选择参数选项,按照下表的内容为即将接受到的请求消息添加一个参数
18. 选则Receive活动,在属性窗口中,点击Content属性后的省略号按钮。在内容定义对话框中,选择参数选项,按照下表的内容为即将接受到的请求消息添加一个参数
属性
|
值
|
Name
|
ProductNumber
|
Type
|
String
|
Assign To
|
productNumber
|
AddItemToCart操作接受的消息包含添加到购物车中商品的产品编码。
19. 添加AddItemToCart.xamlx文件至ShoppingCartService项目。该文件位于*\WCF\Step.by.Step\Solutions\Chapter8文件夹下
AddItemToCart.xamlx文件包含一个自定义的活动(使用活动模板创建)。如果你检查该活动,你将看到该活动包含一个工作流,其逻辑与第七章中创建的AddItemToCart操作相当。将要添加至购物车的产品编码,和购物车将被作为输入参数惨传递至该活动。 该活动执行如下的任务:
- 调用FindItem代码活动, 传递产品编码和购物车对象输入参数。FindItem代码活动确定指定的产品是否已经添加至购物车中。该活动的返回值存贮在变量item中,该变量的类型为ShoppingCartItem
- 如果item的值不为Nothing,那么产品已经添加至购物车,因此购物车中该商品的数量将加1.
- 如果item的值为Nothing,那么产品还没有添加至购物车。If活动的else分支调用GetItemFromDatabase活动以获取该产品的详细信息,然后使用查询到的信息创建一个新的购物车商品对象,最后将该对象存贮到变量item中。AddItemToCollection活动添加至购物车后,Item变量的值为该购物车对象的引用地址。
- 如果if活动的任意一个分支成功,那么Result参数的值将被设置为True。Result参数是一个输出参数,它用以向调用者传输该活动成功或失败。
- 如果异常发生,TryCatch活动的Carches区域将设置Result参数为false,以指明该活动未成功执行。
20. 重新生成解决方案
21. 返回至ShoppingCartService.xamlx文件的设计视图窗口。在Add Item to Cart活动的行为区域,添加一个顺序活动,并按照下表内容设置该顺序活动的属性
属性
|
值
|
Name
|
result
|
Type
|
Boolean
|
Scope
|
Sequence
|
Default
|
False
|
22. 在工具栏的ShoppingCartService区域,选择AddItemToCart活动并添加至Add Item to Cart活动中的顺序活动中。在属性窗口,按照下表的内容设置该活动的属性
属性
|
值
|
ProductNumber
|
productNumber
|
Result
|
Result
|
ShoppingCart
|
shoppingCart
|
23. 在Add Item to Cart的Trigger区域上点击右键,然后选择Receive AddItemToCart Request活动,然后点击创建SendReply
24. 在Add Item to Cart的顺序活动区域上点击右键,然后选择粘贴。如果必要,拖动SendReplyToReceive AddItemToCart活动至AddItemToCart活动位置的下方。然后在属性窗口,更改显示名属性为Send AddItemToCart Response
25. 在AddItemToCart Response活动中,点击Content属性附近的定义连接,将启动内容定义对话框。在出现的对话框中,选择阐述选项,然后按照下表的内容添加一个参数
属性
|
值
|
Name
|
Result
|
Type
|
Boolean
|
Value
|
result
|
该活动在响应消息中回传一个Boolean值,用以指明产品是否成功地添加至购物车中。
你现在已经为ShoppingCartService服务实现了AddItemToCart操作。你可以使用同样的方式实现RemoveItemFromCart,GetShoppingCart和CheckOut操作,但要求在自定义活动上为这些操作上定义逻辑并为每个可能的请求消息添加PickBranch活动。为了节省时间和避免不必要的重复,你可以直接使用本书源代码包含的已完成的ShoppingCartService服务。在本章后续的联系中,都将基于该完成的ShoppingCartService服务
寄宿ShoppingCartService工作流服务
1. 使用Visual Studio,打开*\WCF\Step.by.Step\Solutions\Chapter8\Completed\ShoppingCart文件夹下的ShoppingCart解决方案
2. 打开ShoppingCartService.xamlx文件并切换到设计视图
3. 在Remove Item From Cart活动中的Trigger区域,点击Receive RemoveItemFromCart Request活动。在属性窗口中,确认CanCreateInstance属性复选框未被选中。同样地,Receive GetShoppingCart Request和Receive Checkout Request活动的该属性也未被选中。仅仅只有AddItemTOCart操作能创建一个新的会话
4. 检查Checkout活动的行为区域。当响应消息发送后,一个赋值活动将设置serviceRunning变量为False。该行为致使while活动包含的Pick活动结束,终止工作流,并停止服务实例。
5. 添加一个新的名为ShoppingCarHost的工作流控制台应用项目到ShoppingCart方案
6. 在解决方案窗口中,删除Workflow1.xamlx文件
7. 在项目ShoppingCarHost中引用项目ShoppingCartService
8. 打开Porgramm.cs文件,并添加下面的using语句
using System.ServiceMode.Actitivities;
using System.Xml;
using System.Xml;
9. 在买方法中,添加下面的代码:
WorkflowService service =
XamlServices.Load(@"..\..\..\ShoppingCartService\ShoppingCartService.xamlx") as WorkflowService; WorkflowServiceHost host = new WorkflowServiceHost(service); host.Open(); Console.WriteLine("Service is running. Press ENTER to stop"); Console.ReadLine(); host.Close(); |
10. 打开App.config,添加数据库连接字符串
<connectionStrings>
<add
name="AdventureWorksEntities"
connectionString="metadata=res://*/ProductsModel.csdl|res://*/ProductsModel.ssdl|res://*/ProductsModel.msl;provider=System.Data.SqlClient;provider connection string="data source=localhost;initial catalog=AdventureWorks;integrated security=False; user id=ap_wcf; password=ap_wcf; multipleactiveresultsets=True;App=EntityFramework""
providerName="System.Data.EntityClient" />
</connectionStrings>
11. 添加<system.ServiceModel>片段
<system.serviceModel>
<services>
<service name="ShoppingCartService">
<endpoint address="net.tcp://localhost:8080/ShoppingCartService.xamlx"
binding="netTcpBinding" bindingConfiguration="" contract="IShoppingCartsService" />
</service>
</services>
</system.serviceModel>
12. 重新生成解决方案
为了测试ShoppingCartService,你将使用在第七章中创建的与非持久性服务ShoppingCartService通信的ShoppingCartGUIClient程序。在下面的练习中,你将添加ShoppingCartGUIClient项目到ShoppingCart方案,并配置该程序使用TCP协议连接至ShoppingCartHost,然后测试ShoppingCartService服务
测试ShoppingCartService工作流服务
1. 添加ShoppingCartGUIClient项目到ShoppingCart方案。ShoppingCartGUIClient项目位于*\WCF\Step.by.Step\Solutions\Chapter8\ShoppingGUIClient文件夹下
2. 在方案浏览器窗口中,在项目ShoppingCartGUIClient上点击右键,并点击添加服务引用。在添加服务引用对话框中,点击"Discover"按钮,以自动获取项目中服务;然后在命名空间文本框内输入ShoppingCartService,最后点击确认按钮
上述行为将为客户端程序生成代理对象;客户端使用该代理连接至服务;此外,上述行为还将自动配置客户端。 然后,添加服务引用向导生成的客户端端点信息基于HTTP协议,而ShoppingCartService宿主程序通过TCP协议会外公布服务。因此,你需要手动更新客户端的配置。
3. 打开ShoppingCartGUIClient项目的app.config。删除<system.serviceModel>片段;并添加如下新的配置:
<system.serviceModel>
<client>
<endpoint address="net.tcp://localhost:8080/ProductsService.xamlx"
binding="netTcpBinding"
contract="IProductsService"
name="NetTcpContextBinding_IShoppingCartService" />
</client>
</system.serviceModel>
4. 打开ShoppingCartGUIClient项目的MainWindow.xaml.cs文件,修改Main方法,使其使用新创建的端点连接至服务ShoppingCastService服务private string clientEndPointName = "NetTcpBinding_IShoppingCartService";
5. 设置ShoppingCartGUIClient项目和ShoppingCartHost项目均为方案ShoppingCart的启动项目
6. 生成方案,然后在非调适模式下运行方案。在Shopping Cart GUI客户端窗口中,在产品编码文本框内,输入WB-H098,然后点击"添加"按钮。确认一个水瓶产品添加到购物车中并在客户端程序窗口中显示。
7. 在产品编码中输入SA-M198,然后再次点击"添加"按钮,一个山地车座垫应该被添加至购物车
8. 点击"结算"按钮,确认购物车被清空
9. 关闭Shopping Cart GUI客户端窗口。停止ShoppingCartService服务,然后返回到Visual Studio
关联客户端和服务实例
在第七章中描述了客户端程序和服务如何使用WSHttpContextBinding绑定在请求和响应消息的SOAP头部中传递上下文消息以使客户端程序识别服务实例并连接到该实例。在第七章中讨论集中在持续性服务,但是该原则同样适用于工作流服务。当一个操作被标记为CanCreateInstance,工作流服务宿主程序可以创建一个服务实例,并生成实例ID,然后将该ID作为响应消息的一部分回传给客户端。客户端在后续的请求中,在请求消息的头部信息中提供一个相同的ID,工作流服务宿主程序根据该ID将这些请求分配至正确的服务实例。NetTcpContextBinding绑定提供相同的功能。但是,你也可以使用其他不支持自动关联的绑定来关联服务实例和客户端程序。如果要这么做,你必须服务的配置Receive活动,并指定用于关联请求消息和服务实例的数据。
Receive活动提供CorrelatesOn属性。你可以使用该属性指定一个或者多个消息参数以识别服务实例,而不是识别由工作流运行时生成的实例ID。 当服务的宿主接收到一个请求消息,工作流运行时检查这些参数的值,然后根据这些值分配请求至适当的服务实例,或者创建一个新的服务实例。你应当仔细地确认客户端程序请求消息中对应的字段;否则,关联将不会像期望地那样发生。此外,如果两个或者多个客户端为消息参数提供了相同的信息,那么工作流服务运行时将它们视为同一个客户端,并且将它们全部分配至一个相同的服务实例;如果你需要在客户端之间共享会话数据,那么这种方式非常有效;但是如果该共享不在计划之内,那么它会带来安全方面的风险。
更多关于配置关联更多信息,请参考 Content Based Correlation http://msdn.microsoft.com/en-us/library/ee358755.aspx
创建持续性工作流服务工作流服务的最重要方面是因为他们支持扩展性。与第七章中的程序服务一样,工作流服务可以是持续性的,允许服务实例继续存在即便服务的宿主程序关闭或者重启。此外,你可以配置工作流服务在它空闲一段时间后临时终止服务实例,并将服务实例从内存中释放。如果接收到对该服务实例的请求,工作流可从持久性存贮设备中或获取状态信息,并装载服务实例到内存,然后回复服务实例。
在本章最后一组练习中,你将配置ShoppingCartService服务为持续性服务
配置ShoppingCartService服务为持续性服务
1. 打开ShoppingCartService项目的ShoppingCartService.xamlx文件
2. 在Add Item To Cart活动的动作区域中,点击Send AddItemToCart Response活动,然后在属性窗口中,选中PersistBeforeSend属性对应的复选框
3. 同样地,选中Send RemoveItemFromCart Response活动和Send GetShoppingCart Response活动PersistBeforeSend属性对应的复选框。无论何时,只要服务发送上述其中任意一种消息,服务实例的状态将保存到持久化存储中。
4. 保持Send Checkout Resposne活动的 PersistBeforeSend属性对应的复选框处于未选中状态。
Checkout操作是客户端执行的最后一个操作,当该操作完成后,会话应当终止。若不选中PersistBeforeSend属性,当操作完成时,持久化存储中的状态信息将被移除。
与第七章中描述的持续性服务使用的持久化存储不同,工作流服务使用另一个不同的数据库schema。创建该schema的脚本为存储在C:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en文件夹下SqlWorkflowInstanceStoreLogic.sql和SqlWorkflowInstanceStoreSchema.sql
5. 在文件菜单,选择打开,然后点击文件。在打开文件对话框中设置目录至C:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en;选择SqlWorkflowInstanceStoreSchema.sql文件,然后点击打开按钮。
6. 在数据菜单,指向Transact-SQL编辑器,然后选择连接,在连接数据库引擎对话框中,设置你数据库服务器的信息;然后点击连接。
7. 在Visual Studio工具栏,在数据库下拉列表中,选择WCFPersistence
8. 在数据菜单,执行Transact-SQL编辑器,然后点击执行SQL。确认SQL脚本正确地运行并显示消息"Command(s) completed successfully"
9. 效仿步骤5至步骤8,打开并执行SqlWorkflowInstanceStoreLogic.sql脚本
10. 添加引用System.Activities.DurableInstancing和System.Runtime.DurableInstancing到ShoppingCartHost
11. 打开ShoppingCartHost项目的Program.cs文件,然后添加如下using语句
using System.Activities.DurableInstancing;
12. 修改Main方法
static void Main(string[] args)
{
WorkflowService service =
XamlServices.Load(@"..\..\..\ShoppingCartService\ShoppingCartService.xamlx")
as WorkflowService;
WorkflowServiceHost host = new WorkflowServiceHost(service);
string persistenceStoreConnectionString =
@"Data Source=.\SQLExpress;Initial Catalog=WCFPersistence;Integrated Security=True";
SqlWorkflowInstanceStore instanceStore =
new SqlWorkflowInstanceStore(persistenceStoreConnectionString);
host.DurableInstancingOptions.InstanceStore = instanceStore;
host.Open();
Console.WriteLine("Service running. Press ENTER to stop");
Console.ReadLine();
host.Close();
}
上述代码指定WorkflowServiceHost对象应当支持持久化,并指定连接至持久化数据库的连接字符串。你也可以通过配置文件中通过设置服务行为来提供这些信息。相信信息,请参考MSDN
测试持续性服务
1. 在非调适模式下启动方案。在Shopping Cart GUI客户端窗口中,在产品编码文本框内输入WB-H098,然后点击添加按钮。确认一个水瓶添加至购物车
2. 保持Shopping Cart GUI客户端窗口和服务宿主控制台程序窗口处于运行状态。然后回到Visual Studio
3. 在服务浏览器面板,展开数据连接文件夹,然后展开WCFPersistence数据库的连接,再展开数据表,然后在InstancesTable上点击右键,然后点击显示表内数据
工作流宿主程序在该表中存储会话信息。你将看到该表仅包含一行数据。会话数据以二进制格式存储在PrimitiveDataProperties, ComplesDataProperties, WriteOnlyPrimitiveDataProperties, 和WriteOnlyPerimitiveDataProperties列中。该表中其他的属性提供了关于该服务实例状态的信息
4. 保持Shopping Cart GUI客户端窗口处于运行状态,然后在服务宿主控制台程序窗口中按ENTER键停止服务
5. 在Visual Studio的方案浏览窗口中,选择ShoppingCartHost项目,然后点击右键,指定调适à然后点击启动新实例
6. 返回Shopping Cart GUI客户端窗口,然后在产品编码文本框内输入PU-M044;然后点击添加按钮
一个山地自行车专用鞋子商品将添加至购物车。请注意工作流运行时成功地从持久化存储中的会话状态信息中恢复了购物车中的商品。
7. 点击结算按钮;该操作结束会话,购物车被清空
8. Shopping Cart GUI客户端窗口和服务宿主控制台程序窗口处于运行状态。然后回到Visual Studio。点击InstanceTable标签,然后点击执行SQL按钮。你可以看到现在该表没有任何内容。这是因为当结算操作完成,会话终止,存储在持久化存贮冲的状态信息被移除。
9. 关闭Shopping Cart GUI客户端窗口。在宿主控制台程序中按ENTER键停止ShoppingCartService服务
【总结】
在本章,你已经看到如何使用WF创建工作流服务。WF提供了些许但是完整的消息活动。你可以使用这些活动创建服务,并在服务中几乎能实现所有的消息模式。
你了解到如何使用Receive活动侦听请求,如何使用SendReply活动发送响应。你还学习到如何部署一个工作流服务,以及在工作流中如果通过活动识别和处理错误。 你还学习到如何创建工作流客户端程序;以及如何通过添加服务引用向导创建封装了发送请求消息和等待响应的自定义活动。
最后,你还学习了工作流服务如何关联客户端程序与服务实例,以及如何配置将工作流服务配置为一个持续性服务。
【本章源代码】
由于我查看之前的文章,都没有人下载每章对应的源代码,所以,我一次想将原书所附带的源代码搬家到115网盘。有兴趣的自己去下载, 地址:http://u.115.com/file/aqbx3qjl