工作流通常用在商业事务模型上。正如在现实世界中的商业事务,这些可能运行几秒(在ATM机上转账)或者运行很行时间(从eBay上买些东西,付钱,收货,然后给出反馈)。事务被模型化一次然后将会有成百上千的实例运行,很多是并发运行的。当这些事务中的任何一个运行时,客户端,服务端或者网络会在这期间不可用或者重启。
为了支持长时间运行的事务,需要两个元素: 相关性和持续性。相关性允许一个客户端来确定一它想要与之通信的特定工作流实例。持续性允许一个工作流实例在系统失败时幸存同时允许工作流环境高效地使用内存和CPU资源。WorkflowServiceHost类实现了扩展来支持相关性和持续性。它使用这两个元素实现: 一个在客户端和服务端之间的信道中传递的上下文类,和一个可以将一个工作流实例脱水(写内存数据到磁盘)、加水(从磁盘读取数据到内存)的持续性实例提供者。
在客户端和服务端传递的上下文唯一地定义了工作流实例。当客户端发送一条消息给一个WF开启的服务时,消息被检测来查看是否存在一个上下文实例。如果没有上下文实例,会在WF运行时中创建一个新的工作流实例。如果消息中有一个上下文实例,消息会被发送到一个已有的工作流实例上。WF运行时会检查这个实例是否在内存中,如果不在内存中,它会调用持续性实例提供者来从磁盘读取。消息然后被反序列化一个.NET 类型并传递给WF运行时然后分发到合适的实例上。
注意WF运行时在一个单一的WCF服务实例中被完全加密,同时对工作流实例和一致性负责。换句话说,如果一个工作流有50个实例在运行,从所有工作流流中发送的消息会汇总到一个单一的工作流实例。工作流运行时有它自己的用来处理工作流实例的相关性和消息队列的内部结构。实例ID存储在上下文中,与持续实例提供者连接起来保证工作流运行时在内存中有正确的实例。
长时间运行工作流
在这一部分,我们将创建一个接受并执行销售记录的商业过程模型。为了显而易见的原因,已经在工作流之前完成欺骗检测。如果一次交易被怀疑,它会被中转到一个分析器来检查。如果它看起来很好分析,订单会继续进行;否则,订单会被拒绝。在这个例子中的大多数工作流实例将会在几秒钟内完成,因为大多数交易请求不是欺诈性的。但是当一个接受到的请求需要人工审查时,过程将会花费几分钟或者几小时才能完成。在一个繁忙的交易日,数以百计的货物交易可能需要审查。对每一个货物交易来说过程都是一致的,但是细节(输入和输出)将会不同。
在这个场景中,我们虚构的货物交易使用一个网页或者富客户端应用调用InitiateTrade方法执行一个新的货物交易。如果返回的状态是executed,她的交易就完成了。如果状态时review, 那么交易没有完成同时她会期待在短期内收到一个通知(这个例子没有显示任何通知,但是你可以想象一封e-mail, 一个电话,一条短信息或者即时消息)。如果状态是review,那么在世界的其他地方,一个财务分师会被提醒有一个货物交易需要审查。分析师使用一个网页或者富客户端应用来查看交易请求然后调用Approval 方法来指示是否系统应该执行交易。
为这个场景重新调用列表11.3的接口。它包含三种消息格式和两个服务操作。消息是TradeRequest, TradeRequestStatus, ApprovalReques. 客户端发送一个TradeRequest来初始化一个新的交易并发送一个ApprovalRequest来允许或者拒绝一次有问题的交易。客户端与工作流通信,从实例到终端,将会使用这些消息显式实现。接口包含了两个服务操作: InitiateTrade和Approval, 每个使用消息的格式。
图片11.10显示了工作流的开始。接收活动被命名为InitiateTrade,它是一个由三个代码活动组成的混合活动,其中的两个包含在在一个IfThenElse结构中。CheckFraud活动调用一个运行时来评估可能有欺诈的交易。如果它看起来没有问题,ExecuteTrade活动被调用来执行货物交易然后返回一个RequestTradeStatus结构同时其内部属性status=="executed".如果交易潜在是具有欺骗性质的,它会被发送到一个工作队列中去,然后返回一个带有确认号码且status=="review"的结构。这三个代码活动在接收活动中同步运行,所以无论选择哪个分支,会在一个时间行为中返回一个反馈。
图片11.10 在一个合成接收活动中的一个长时间运行工作流
这个工作流的配置文件在列表11.6中显示。由于这是服务开启工作流所需要的,在使用wsHttpContextBinding时会在信道中使用ContextBinding.当TradeRequest消息被服务接收到以后,一个内部的WF行为会检测这条消息来查看其内部是否包含上下文。如果没有上下文,WorkflowRuntime 创建一个工作流的实例。当TradeRequestStatus同步消息从接受活动发送回客户端时,WF行为使用一个上下文给消息加一个标记,指示工作流的实例ID.
我们继续之前的例子,如果交易请求没有立即执行,工作流会暂停,监听对货物交易请求的人工审查结果。图片11.11显示了剩下的工作流过程。监听活动有两个分支: 一个是延时操作另外一个是一个接收操作。如果接收活动在没有在延时活动之前调用,工作流将会继续但没有输出。比如初始化InitiateTrade活动,Approval活动也是一个组合活动,但是这次它内部只有一个代码活动。ReviewReceive代码检查ApprovalRequest消息来查看交易是否完成。如果完成,它会设置一个标志指示交易应该被执行。不考虑交易是否在ExecuteTrade2中执行,NotifyCustomer活动向初始化交易的客户发送一封e-mail.
在图片11.10和11.11中描述的工作流有两个接收活动,标记为InitiateTrade和Approval.InitiateTrade的CanCreateInstance属性被设置为true,意味着这个操作可以不需要在上下文中传递一个实例ID就能调用。当WF接收消息以后,它将创建工作流程序的一个新的实例。Approval活动的CanCreateInstance属性被设置为false, 意味着这个操作不能被调用如果上下文中没有一个实例ID。如果一个客户端尝试不适用实例ID来调用它,它将会接收到一个SOAP错误。
列表11.8显示了实现这个工作流的代码。一个代码活动,codeCheckecFraud_ExecuteCode, 用来调用内部认证规则并设置从接收位置的返回值。另外一个代码活动,codeNeedsReview_ExecuteCode_1, 用来调用内部规则来存储上下文。一个第三方代码活动,ReviewReceived_ExecuteCode, 用来检测发送的允许消息并相应地设置验证。这个内部规则,saveContext,在这个例子中总体上是简单的冰将InstanceId存储到一个文件中。在生产环境中,内部规则应该用来在一个可以从客户端和服务端访问的数据库中或者网络服务中存储键值对。
图片11.11 完成一个长时间运行的工作流
列表11.8 一个长时间运行工作流的代码
namespace LRWorkflowService { [DataContract] public class TradeRequest { [DataMember] public string account; [DataMember] public string action; [DataMember] public string ticker; [DataMember] public double price; } [DataContract] public class ApprovalRequest { [DataMember] public string tradeNumber; [DataMember] public string approval; [DataMember] public string reason; } [DataContract] public class TradeRequestStatus { [DataMember] public string confirmationNumber; [DataMember] public string status; } public class wfHelper { public const string INSTANCEFILENAME = @"c:\temp\instanceID.txt"; public bool Evaluate(TradeRequest tradeRequest) { if (tradeRequest.account == "000") { return false; } else { return true; } } public bool Evaluate(ApprovalRequest approvalRequest) { if (approvalRequest.approval == "OK") { return true; } else { return false; } } //This only works in demo/debug since the client and service //have access to the folder where key/instanceID is stored. //For real, it should use WS that the client and service can reach public void SaveContext(string key, string instanceId) { if (File.Exists(INSTANCEFILENAME)) { File.Delete(INSTANCEFILENAME); } string txt = string.Format("{0},{1}", key, instanceId); File.WriteAllText(INSTANCEFILENAME, txt); } } public partial class StockService : SequentialWorkflowActivity { public StockService() { InitializeComponent(); } public bool bValidation; public TradeRequestStatus receiveActivity1_ReturnValue_1; public ApprovalRequest receiveActivity2_approvalRequest1; public TradeRequest receiveActivity1_tradeRequest1; private void CheckFraud_ExecuteCode(object sender, EventArgs e) { wfHelper h = new wfHelper(); bValidation = h.Evaluate(receiveActivity1_tradeRequest1); receiveActivity1_ReturnValue_1 = new TradeRequestStatus(); receiveActivity1_ReturnValue_1.confirmationNumber = "123"; if (bValidation) { receiveActivity1_ReturnValue_1.status = "OK"; } else { receiveActivity1_ReturnValue_1.status = "Review"; } } private void NeedsReview_ExecuteCode(object sender, EventArgs e) { wfHelper h = new wfHelper(); h.SaveContext(receiveActivity1_ReturnValue_1.confirmationNumber, this.WorkflowInstanceId.ToString()); } private void ReviewReceived_ExecuteCode(object sender, EventArgs e) { wfHelper h = new wfHelper(); bValidation = h.Evaluate(receiveActivity2_approvalRequest1); } } }