首页 > 编程开发 > Objective-C编程 >
-
WF编程系列之工作流中的通讯
独立存在的工作流并不是很多。大多数工作流还是需要和本地或远程的服务通信来完成它们的工作。在前面的章节中,我们已经看到了在WF中的一些基本的构造块。这些构造块包括了诸如HandleExternalEvent和WebServiceInput这样的活动。
本地通信服务回顾
我们知道,本地通信服务允许工作流和它们的宿主进程交换数据。在第3章,我们定义了一个服务来触发BugAdded事件到正在运行的工作流,这将依次调用服务的AssignBug方法。该服务通过事件发送数据到工作流,而工作流则通过调用方法发送数据到服务。
上面的截图展示了工作流运行时是如何扮演本地通信服务和工作流实例之间的那道屏障的。运行时从本地服务截取事件,并沿着这些事件跳转到恰当的工作流实例上。这种交互是必须的,因为等待事件的工作流实例可能已经从内存中被卸载了,并持久化到数据库表中。运行时可以在发送事件的时候要求持久化服务重新加载工作流,但它首先会需要一个工作流实例ID。即使工作流仍然在内存中,运行时还是需要一个实例ID来定位合适的工作流。运行时所使用的这个实例ID,是ExternalDataEventArgs对象在事件传递期间所需要的。
在许多方面,我们可以把实例ID认为是在寄送包裹上的街道地址。给出一个街道地址,我们就可以将包裹寄送到正确的房子。如果有人居住在这里,我们就能完全肯定我们已经把包裹寄送给了预定接收者。然而一旦街道地址把我们带到了一件办公楼又如何是好呢?我们并没有充足的信息来把包裹与它的预定接收者联系起来。
我们在第3章的简单范例只具有一个等待事件的活动。运行时不需要任何额外的信息发送这些有效内容。并不是所有的工作流都能这样简单。我们需要学会,一旦存在等待一个事件的多个活动,那么我们该如何把这些消息联系起来
相关性参数
WF使用相关性记号来确定在工作流中的特定活动和宿主中的本地通信服务之间的会话。通信接口默认是不具有相关性的,并且在工作流具有多个活动并发等待即将到来的事件时,我们只需要确定这些相关性记号就可以了。让我们看一个例子:
想象一下为我们的bug跟踪的应用程序开发一个工作流,这将要求小组中的成员对即将到来的一个bug进行投票。投票yes表示小组成员接受系统中的这个bug。投票no表示小组成员想要关闭这个bug。我们可能会像下面这样设计接口和事件参数类:
[ExternalDataExchange]
public interface IBugVotingService
{
void RequestVote(string userName);
event EventHandler<VoteCompletedEventArgs> VoteCompleted;
}
[Serializable]
public class VoteCompletedEventArgs : ExternalDataEventArgs
{
public VoteCompletedEventArgs(Guid instanceID, string userName, bool isYesVote)
: base(instanceID)
{
_userName = userName;
_isYesVote = isYesVote;
}
private string _userName;
public string UserName
{
get { return _userName; }
set { _userName = value; }
}
private bool _isYesVote;
public bool IsYesVote
{
get { return _isYesVote; }
set { _isYesVote = value; }
}
}
工作流将会调用外部方法RequestVote,并把用户名作为参数传递。服务有责任通知用户并等待收集用户的投票。服务还能将投票结果打包到一个事件参数对象中并触发VoteCompleted事件。工作流将会接受这个事件,截取该事件的参数,并决定下一步要做什么。
现在想象一个工作流,就像下面截图中的那样:
当我们设计我们的服务接口时,我们假设工作流只会寻找到一个单独的投票。上面截图中的工作流在平行地寻找两个投票。该工作流将调用服务来请求小组技术组长的投票。该工作流还将调用服务来请求来自小组分析师的投票。然后在这个平行的活动完成之前,它会等待这些事件的到达。
注意:ParallelActivity活动,我们在第4章介绍过,并没有提供真正的平行处理。运行时只允许在一个工作流实例中每次执行一个线程。这里的平行活动将使用一个线程按照不确定的顺序执行这两个分支,直到它们阻塞或等待一个事件。既然这与投票小组中的一个成员要在很短的时间内作出投票响应是不同的,这两种平行活动的分支将会到达它们被阻塞的地方或等待它们各自事件的到达。
设想一下,小组的技术组长是第一个响应投票的。服务将会触发一个事件,工作流运行时将会截取这个事件。然后运行时将找到我们的工作流实例并发送这个事件。问题是——运行时将会把技术组长的投票发送到哪个分支的HandleExternalEvent活动呢,是左边的那个分支,还是右边的呢?我们不能肯定,技术组长的投票可能结束右边的分支,也就是我们为处理分析师的投票而设计的分支。这种场景代表了我们必须为运行时提供更多信息才能解决的问题。
相关性特性
当工作流是一个RequestVote方法时,它会包括一个重要的信息——用户名参数。如果我们请求Scott的投票,那么我们应该在下面的活动中得到Scott的投票回复。用户名参数可以和活动结合在一起——我们只需要通知工作流这个参数的重要性。下面的代码是我们的服务接口的修正版本,以及事件参数类:
[ExternalDataExchange]
[CorrelationParameter("userName")]
public interface IBugVotingService
{
[CorrelationAlias("userName", "e.UserName")]
void RequestVote(string userName);
[CorrelationInitializer]
event EventHandler<VoteCompletedEventArgs> VoteCompleted;
}
[Serializable]
public class VoteCompletedEventArgs : ExternalDataEventArgs
{
public VoteCompletedEventArgs(Guid instanceID, string userName, bool isYesVote)
: base(instanceID)
{
_userName = userName;
_isYesVote = isYesVote;
}
private string _userName;
public string UserName
{
get { return _userName; }
set { _userName = value; }
}
private bool _isYesVote;
public bool IsYesVote
{
get { return _isYesVote; }
set { _isYesVote = value; }
}
}
在我们的服务接口上,我们具有三个新的特性。这些特性包括了运行时用于在我们的服务和工作流中独立的活动之间建立会话的相关性元数据。
相关性参数
我们把CorrelationParameter特性添加到了我们的接口中。这个特性指定了参数的名称,运行时根据它将一个事件映射到一个特定的HandleExternalEvent活动。我们将userName指定为我们的相关性参数。运行时将会在通信服务接口中所有的方法和事件上寻找带有userName名称的参数。当它发现了这样一个参数,它就会把这个参数用于相关性。
如果有多个相关性参数,那么CorrelationParameter特性就可以在一个接口上多次出现,我们只能在接口类型上使用这个特性。
相关性初始化
我们已经对RequestVote方法使用了CorrelationInitializer特性进行装饰。工作流运行时会把对RequestVote的调用识别为工作流和通信服务之间会话的开始。运行时还会把用户名参数识别为相关性参数,并保存这个值。随后,当事件到达工作流时,工作流将会对保存的值和即将收到的相关性参数进行比较。
有一个明显的问题,对于一个事件而言,相关性参数是什么?工作流又是如何知道的?这里并没有用户名参数——我们在事件参数中封装了用户名。这就是第三个特性CorrelationAlias所在的地方。
相关性别名
我们可以把CorrelationAlias特性应用到服务接口中的方法和事件上,从而在独立的成员上覆盖CorrelationParameter特性。我们把这个特性放在VoteCompleted事件上。这个特性告诉运行时从参数e的UserName属性中取出用户名的相关性参数。
我们建立了我们在服务接口中所需要的所有元数据。工作流运行时将具有充分的信息来把传递scott用户名的CallExternalMethod活动与等待scott投票结果的HandleExternalEvent活动相关联。我们下一步要做的是优化工作流中的相关性元数据。
相关性记号
当我们把CallExternalMethod活动拖放到工作流设计器中时,我们至少要配置InterfaceType和MethodName属性。这些属性会告诉WF我们想要活动调用什么服务和服务方法。如果我们配置的接口具有一个CorrelationParameter特性,那么设计器将添加一个新的属性到Properties窗口——CorrelationToken属性(参见下面的截图)。
WF在会话中使用相关性记号来连接活动。我们需要为这个记号指定名称和所有者。记号的名称是随意的,但由于它标志了该记号的身份,我们应该选择一个有意义的名称。所有者必须是当前活动的祖先。在上面的截图中,我们将这个相关性记号标记为TechLead,并将它的祖先——平行活动的分支指派为这个记号的所有者。
HandleExternalEvent活动是这个分支中的下一个活动,而这个活动应该处理技术组长的投票。一旦我们在这个活动中指派了InterfaceType和EventName属性,我们将再次看到CorrelationToken属性出现。我们将要在记号的下拉清单中选择TechLead。
我们在左边的分支里把相关性记号与CallExternalMethod活动和HandleExternalEvent活动连接起来。我们也能在右边的分支中为这对活动创建一个新的记号。工作流运行时将使用这些记号和用户名参数来保证它把投票发送到合适的事件处理程序。
尽管相关性记号在连接活动时很有用,我们不应该将其作为安全特性而依赖。只为,如果一个投票说它是来自技术组长,但并不表示这个投票真的来自于技术组长。在下一节,我们将学会如何使用HandleExternalEvent活动的另一个属性——Role。
基于角色的授权
保护计算机资源的过程包括两个步骤。第一步是验证。验证确定了用户的身份。验证可能很简单,就像询问一个人的用户名和密码那样,或者更多地利用相关的生物特征信息,如指纹识别和视网膜扫描。WF不验证用户,但是当需要验证时,运行时将会依赖于软件中的验证机制。例如,以ASP.NET应用程序为宿主的工作流可能需要集成Windows验证的支持,或者可能依赖于ASP.NET的membership provider来对用户进行验证。
一旦用户的身份得到确定,我们就可以决定允许用户执行什么操作。这就是第二步——授权。授权规则通常听起来像是——只有经理才能审批报销单,或只有管理员才能取消操作。注意到这些规则涉及到像经理或管理员这样的用户分组。这是因为我们代表性地为用户指派角色并对它们基于角色的请求进行授权(这里,“基于角色的授权”是一个术语)。
在软件的世界中,角色管理软件别有一番风味。对于Windows领域中的软件而言,我们可能从它们的活动目录组成员中得到用户的角色。在面向公众的 ASP.NET的web应用程序中,我们可以从ASP.NET的Role Provider中得到用户的角色。
为了支持不同的角色实现,WF提供了一个可扩展的角色管理方案。WF提供了一个名为WorkflowRole的抽象基类。这个类的内嵌实现为Windows活动目录和ASP.NET 2.0角色提供者提供了角色管理:
角色和活动
对基于角色的授权提供支持的两个WF活动是HandleExternalEvent活动和WebServiceInput活动,它们都暴露了具有WorkflowRoleCollection类型的Roles属性。如果我们想要使用基于角色的授权来保护这些活动,我们将需要邦定Roles属性到一个有效的集合上。在下面的代码中,我们声明了一个名为ValidRoles的公有字段。在工作流的Initialized事件处理程序中,我们新添加了一个单独的WebWorkflowRole实例到validRoles集合中。
public partial class BugFlowWithRoles : SequentialWorkflowActivity
{
public WorkflowRoleCollection validRoles = new WorkflowRoleCollection();
private void BugFlowWithRoles_initialized(object sender, EventArgs e)
{
validRoles.Add(new WebWorkflowRole("TechLeads"));
}
}
在角色的位置上,我们需要配置我们的活动来利用角色。
当一个本地通信服务宿主为工作流触发一个事件时,它可以在ExternalDataEventArgs对象的Identity属性中传递Windows身份参数。该活动将会对指派了身份的角色和Role集合中的角色进行比较。如果匹配,那么活动将继续执行;如果不匹配,那么活动将抛出一个WorkflowAuthorizationException异常。我们可以在工作流中使用错误处理来管理这个异常,或者让该异常终止工作流。注意到如果本地通信服务没有在事件参数中显示地传递一个身份,那么工作流将使用与当前线程相关联的身份。
当我们使用CallExternalMethod和HandleExternalEvent活动来和宿主进行通信时,我们正工作在一个比较高的抽象级别上。像基于角色的授权和交互性这样的内嵌特性,可以解决很多与消息系统关联的头痛之事。然而,在某些情况下,当一个设计需要额外的弹性时,就需要更接近本质(close to the metal)。在下一节,我们将看一下在这些高级通信活动之下的队列机制。
工作流队列
如果你回顾一下本章开头的图表,就会记得关于工作流运行时截取事件的会话。在一个特定的工作流实例中把事件发送到恰当的活动,这是运行时的职责。但是工作流如何发送一个事件呢?运行时不能只在一个任意的线程上触发事件——在一个实例中每次只有一个线程能够执行。
答案是一个查询服务,它是工作流运行时的一部分。活动使用这个服务来创建队列,这个队列可以保存即将到来的数据。当一个项到达队列中时,一个活动就会订阅这个通知。这些队列成为工作流的一部分,在持久层服务序列化并保存这个工作流实例时,和工作流一起被序列化。这就是为什么我们的数据交换的事件参数被标记为可序列化的原因之一。
当运行时发送一个事件到工作流时,它会为该事件取出正确的队列,并在这个队列上添加事件参数。当选择了正确的队列时,每个队列都会暴露一些信息,包括那些允许运行时使类型和相关性参数匹配的信息。在下面的截图的底部,我们暗示了运行时将使用工作流队列名称来路由(route)事件。我们将在下一节中看到关于队列名称的细节。
WorkflowQueue和WorkflowQueueInfo
下面的截图显示了WorkflowQueue类,它代表了在工作流实例中的一个队列;还显示了WorkflowQueueInfo类,它描述了一个队列。代表性地,当我们使用诸如HandleExternalEvent或Delay这样的事件侦听活动时,我们不需要知道这些底层的队列。然而,这些队列不能支持大量的场景,它们在更高级的抽象上是不可能的。让我们看一些例子。
查找和等待活动
让我们回到本章开始的那个工作流。在工作流中,我们在Parallel活动的两个分支中使用了HandleExternalEvent活动。这些事件处理程序等待来自本地通信服务的投票事件到达,并使用相关性记号来保证投票到达正确的分支。我们将在事件处理程序中为运行时的WorkflowIdled事件调用下面的代码。当工作流被阻塞并等待事件到达时,运行时触发这个事件。
static void DumpQueueInfo(WorkflowInstance workflow)
{
ReadOnlyCollection<WorkflowQueueInfo> queueInfos;
queueInfos = workflow.GetWorkflowQueueData();
Console.WriteLine("Queue Info for {0}", workflow.InstanceId);
for (int i = 0; i < queueInfos.Count; i++)
{
Console.WriteLine();
Console.WriteLine("Queue #{0}", i.ToString());
Console.WriteLine(queueInfos[i].QueueName);
Console.WriteLine("Subscribed activities: ");
ReadOnlyCollection<string> names = queueInfos[i].SubscribedActivityNames;
foreach (string name in names)
{
Console.Write("{0} ", name);
}
Console.WriteLine();
Console.WriteLine();
}
}
我们在第3章的简单范例只具有一个等待事件的活动。运行时不需要任何额外的信息发送这些有效内容。并不是所有的工作流都能这样简单。我们需要学会,一旦存在等待一个事件的多个活动,那么我们该如何把这些消息联系起来
相关性参数
WF使用相关性记号来确定在工作流中的特定活动和宿主中的本地通信服务之间的会话。通信接口默认是不具有相关性的,并且在工作流具有多个活动并发等待即将到来的事件时,我们只需要确定这些相关性记号就可以了。让我们看一个例子:
想象一下为我们的bug跟踪的应用程序开发一个工作流,这将要求小组中的成员对即将到来的一个bug进行投票。投票yes表示小组成员接受系统中的这个bug。投票no表示小组成员想要关闭这个bug。我们可能会像下面这样设计接口和事件参数类:
[ExternalDataExchange]
public interface IBugVotingService
{
void RequestVote(string userName);
event EventHandler<VoteCompletedEventArgs> VoteCompleted;
}
[Serializable]
public class VoteCompletedEventArgs : ExternalDataEventArgs
{
public VoteCompletedEventArgs(Guid instanceID, string userName, bool isYesVote)
: base(instanceID)
{
_userName = userName;
_isYesVote = isYesVote;
}
private string _userName;
public string UserName
{
get { return _userName; }
set { _userName = value; }
}
private bool _isYesVote;
public bool IsYesVote
{
get { return _isYesVote; }
set { _isYesVote = value; }
}
}