WF 4 持久化、跟踪和书签:实用方法






4.72/5 (29投票s)
展示了关于 WF 4 持久化、跟踪和书签的实用示例。
引言
本文不试图深入探讨过多的理论。本文的目的是展示 WF 4.0 持久化、跟踪和书签的实际工作示例。
如果您看过我之前的任何文章,您会知道我通常会花很多时间来解释理论,然后再进行实践。在这里,我发现这样做意义不大,原因只有一个:本文讨论的主题的理论在许多资源和书籍中都有很好的涵盖,但我发现——在我写这篇文章的时候——实用的有用的例子并不容易找到。因此,我决定编写简单但足够的示例来展示这些功能。我希望我的尝试能让尽可能多的人受益。
WF 4.0 持久化
第一步是设置新的持久化数据库。转到“C:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en”目录,您会找到两个文件
- SqlWorkflowInstanceStoreSchema.sql
- SqlWorkflowInstanceStoreLogic.sql
在 SQL Server (Express 或以上版本) 中,创建一个具有方便名称的数据库(我的数据库名为“SqlWorkflowInstanceStore”),然后运行上述脚本来创建所需的对象。
WF 4 中的持久化模型与 WF 3.5 不同。新模型现在称为 InstanceStore 而不是 PersistenceProvider。对于 WF 3.5,PersistenceProvider 数据库仍然在相同的位置可用
- SqlPersistenceProviderLogic.sql
- SqlPersistenceProviderSchema.sql
在 WF 3.5 中,有一个用于管理工作流的内置服务,例如唤醒工作流实例… 该服务的 SQL 文件仍然在相同的位置可用
- SqlPersistenceService_Logic.sql
- SqlPersistenceService_Schema.sql
跟踪将在后面介绍,但为了完整起见,在 4.0 中,没有一个跟踪参与者会写入 SQL。Tracking_Schema.sql 适用于 3.5 工作流。开箱即用的 WF 4.0,有一个跟踪参与者会写入 ETW (Windows 事件跟踪)。同样,WF 3.5 的这些文件是
- Tracking_Schema.sql
- Tracking_Logic.sql
构建 WF
创建一个新的控制台工作流应用程序,并使用默认的Workflow1.xaml来构建以下简单的工作流
Receive 和 SendReplyToReceive 形状接收一个空请求并向调用者返回工作流实例 ID。CodeAcitivty1
是一个自定义活动,用于检索工作流实例 ID。下面是此活动的 C# 代码
public sealed class CodeActivity1 : CodeActivity
{
public OutArgument<Guid> WFInstanceId { get; set; }
protected override void Execute(CodeActivityContext context)
{
context.SetValue(WFInstanceId, context.WorkflowInstanceId);
}
}
关键在于设置 SendReplyToReceive 形状的 PersistBeforeSend
属性。这将导致 WF 在向调用者发送响应之前进行持久化。这意味着现在 WF 将有一个可以返回的点,以防 WF 被挂起或卸载… 稍后将详细介绍。
Delay
活动用于将 WF 延迟 1 分钟… 稍后我们将看到为什么需要这样做。
现在,转到Program.cs文件,让我们编写托管 WF 和设置持久化所需的代码。
使用以下代码,然后我将对其进行解释
class Program
{
static void Main(string[] args)
{
//WorkflowInvoker.Invoke(new Workflow1());
string baseAddress = "https://:8089/TestWF";
using (WorkflowServiceHost host =
new WorkflowServiceHost(new Workflow1(), new Uri(baseAddress)))
{
host.Description.Behaviors.Add(new
ServiceMetadataBehavior() { HttpGetEnabled = true });
host.AddServiceEndpoint("IService", new BasicHttpBinding(), baseAddress);
SqlWorkflowInstanceStore instanceStore = new SqlWorkflowInstanceStore(
@"Data Source=.\SQL2008;Initial Catalog=" +
@"SqlWorkflowInstanceStore;Integrated Security=True");
host.DurableInstancingOptions.InstanceStore = instanceStore;
host.Open();
Console.WriteLine("Car rental service listening at: " +
baseAddress);
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
}
}
}
默认情况下,WorkflowInvoker
用于调用 WF。我将其更改为WorkflowServiceHost
,原因是WorkflowInvoker
无法配置持久化。WorkflowServiceHost
用于托管 WF 服务。另一种调用(托管)工作流的方法是WorkflowApplication
,它(与WorkflowServiceHost
一样)可用于托管具有扩展(持久化、跟踪)的长时异步工作流;但是,仅适用于非服务的工作流。
创建我的 WCF 端点并将其添加到主机后,我使用SqlWorkflowInstaceStore
类来配置我的持久化存储,这就是我开始时准备的存储。
创建客户端
运行 WF,以便主机正在运行,并且 WCF 端点正在监听“https://:8089/TestWF”的请求。
现在创建一个客户端(控制台)应用程序,并向上述 URL 添加服务引用。编写以下代码来调用服务操作
ServiceReference1.ServiceClient proxy = new ServiceReference1.ServiceClient();
Guid workflowId = (Guid)proxy.Operation1();
Console.WriteLine("client done");
Console.WriteLine("enter key");
Console.ReadLine();
运行客户端并观察场景:一旦调用了操作(并返回了工作流实例 ID),请打开“InstancesTable”表,您将看到一个已持久化的 WF 的记录。为什么会这样?回想一下,我们在 SendReply 形状上设置了一个属性,使其在发送回复之前进行持久化。为此,WF 已将状态持久化到数据库,但仍在运行,因为我们没有挂起或卸载 WF。请参见下图
此记录会在数据库中保留多长时间?保留 1 分钟,因为我们在 WF 中有一个 Delay 形状将完成延迟 1 分钟,之后 WF 执行将完成,记录将从数据库中消失。
接下来,我将添加支持(行为)以持久化和卸载 WF,并添加控制端点以在 WF 上执行远程操作。
关于持久化和控制端点的更多内容
总之,到目前为止,我已经为WorkflowServiceHost
托管的 WF 4.0 工作流配置了持久化存储。我展示了工作流如何在向客户端应用程序发送响应之前进行持久化,并观察了数据库中的持久化状态。
现在,我将添加一个主机行为,该行为根据空闲时间持久化工作流,而不是在工作流自身发送响应之前;此外,我还将添加一个主机行为,该行为在指定的时间间隔后卸载工作流,从而展示仅持久化工作流与从内存中卸载工作流之间的区别。
让我们从取消选中(禁用)SendReplyToReceive 形状的 PersistBeforeSend
属性开始。接下来,在工作流应用程序的Program.cs文件中,在“host.Open()
”语句之前添加以下代码
host.Description.Behaviors.Add(new WorkflowIdleBehavior()
{
TimeToPersist = TimeSpan.FromSeconds(5),
TimeToUnload = TimeSpan.FromSeconds(20)
});
此代码添加了一个行为,该行为将在 5 秒不活动后持久化 WF 实例,并在 20 秒不活动后完全卸载 WF 实例。有什么区别?持久化——如前所述——将工作流状态持久化到数据库,但保持工作流实例运行。而卸载则执行两项操作:它持久化 WF 状态,并将其从内存中卸载;通常用于长时间运行的进程。
构建 WF 并运行它。它已准备好监听其端点的请求。
现在运行客户端。一旦服务将响应发送回客户端(请记住,此处发送时没有持久化),它将进入一个 1 分钟的延迟形状。同时,我们已配置在 5 秒后持久化 WF。所以等待 5 秒,然后打开“InstancesTable”表;就像在第一部分一样,您将看到 WF 状态已被持久化
但是,现在我们还配置了 WF 在 20 秒后卸载。因此,在额外等待 15 秒后,这次打开“RunnableInstancesTable”表,您将看到对应于同一工作流的记录。为什么是“RunnableInstancesTable”表?因为这次 WF 不仅被持久化,而且还被卸载了,这意味着它已准备好再次加载并继续执行
延迟形状完成后,WF 实例将被重新加载到内存并完成执行。数据库中的两处记录都将消失。
最后,让我们向我们的 WF 添加一个控制端点。控制端点允许从客户端向 WF 实例发送命令。在主机应用程序中添加以下代码
WorkflowControlEndpoint controlEndpoint =
new WorkflowControlEndpoint(
new BasicHttpBinding(),
new EndpointAddress(new Uri(baseAddress) + "/wce")
);
host.AddServiceEndpoint(controlEndpoint);
此代码添加了一种特殊类型的端点,称为控制端点,其地址为:“https://:8089/TestWF/wce”。
运行服务,并在客户端应用程序中更新服务引用。在调用服务操作后,在客户端应用程序中添加以下代码
System.Threading.Thread.Sleep(new TimeSpan(0, 0, 30));
WorkflowControlEndpoint ep = new WorkflowControlEndpoint(new BasicHttpBinding(),
new EndpointAddress("https://:8089/TestWF/wce"));
WorkflowControlClient client = new WorkflowControlClient(ep);
client.Terminate(workflowId);
此代码创建一个工作流控制客户端,并使用工作流实例 ID 发送命令以终止 WF 实例。为了测试这一点,我让线程休眠了 30 秒。这样,我就可以确保此时 WF 实例已被卸载(请查看 WF 配置)。现在,当我发送 Terminate 命令时,请注意 WF 实例如何没有继续执行而是被终止。为了确保这一点,请查看数据库持久化记录如何在仅 30 秒后被删除,而不是像之前解释的那样保留 1 分钟。
WF 4.0 跟踪
在本节中,我将对同一示例进行一些修改以添加跟踪支持。
下面是我们的简单工作流(Workflow1.xaml)
它从客户端应用程序接收请求。使用自定义活动,它检索工作流实例 ID 并将其作为响应返回给客户端。最后,一个 Delay 形状将执行延迟 30 秒。
下面是自定义活动的 C# 代码
public sealed class CodeActivity1 : CodeActivity
{
public OutArgument<Guid> WFInstanceId { get; set; }
protected override void Execute(CodeActivityContext context)
{
context.SetValue(WFInstanceId, context.WorkflowInstanceId);
CustomTrackingRecord customRecord = new CustomTrackingRecord("CustomInfo")
{
Data =
{
{"Date", DateTime.Now.ToShortDateString()},
}
};
context.Track(customRecord);
}
}
自定义活动返回工作流实例 ID 并创建一个新的CustomTrackingRecord
。CustomTrackingRecord
是您想要跟踪的任何自定义信息。在这个简单的示例中,我正在跟踪一个存储当前日期的自定义变量… 稍后将详细介绍。
下面是Program.cs文件中设置主机和跟踪所需的 C# 代码
class Program
{
static void Main(string[] args)
{
string baseAddress = "https://:8089/TestWF";
using (WorkflowServiceHost host =
new WorkflowServiceHost(new Workflow1(), new Uri(baseAddress)))
{
host.Description.Behaviors.Add(new
ServiceMetadataBehavior() { HttpGetEnabled = true });
host.Description.Behaviors.Add(
new ServiceBehaviorAttribute() { IncludeExceptionDetailInFaults = true });
host.AddServiceEndpoint("IService", new BasicHttpBinding(), baseAddress);
TrackingProfile fileTrackingProfile = new TrackingProfile();
fileTrackingProfile.Queries.Add(new WorkflowInstanceQuery
{
States = { "*" }
});
fileTrackingProfile.Queries.Add(new ActivityStateQuery()
{
ActivityName = "*",
States = { "*" },
// You can use the following to specify specific stages:
// States = {
// ActivityStates.Executing,
// ActivityStates.Closed
//},
Variables =
{
{ "*" }
} // or you can enter specific variable names instead of "*"
});
fileTrackingProfile.Queries.Add(new CustomTrackingQuery()
{
ActivityName = "*",
Name = "*"
});
FileTrackingParticipant fileTrackingParticipant =
new FileTrackingParticipant();
fileTrackingParticipant.TrackingProfile = fileTrackingProfile;
host.WorkflowExtensions.Add(fileTrackingParticipant);
host.Description.Behaviors.Add(new WorkflowIdleBehavior()
{
TimeToPersist = TimeSpan.FromSeconds(5),
TimeToUnload = TimeSpan.FromSeconds(20)
});
host.Open();
Console.WriteLine("Car rental service listening at: " +
baseAddress);
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
}
}
}
public class FileTrackingParticipant : TrackingParticipant
{
string fileName;
protected override void Track(TrackingRecord record,
TimeSpan timeout)
{
fileName = @"c:\tracking\" + record.InstanceId + ".tracking";
using (StreamWriter sw = File.AppendText(fileName))
{
WorkflowInstanceRecord workflowInstanceRecord =
record as WorkflowInstanceRecord;
if (workflowInstanceRecord != null)
{
sw.WriteLine("------WorkflowInstanceRecord------");
sw.WriteLine("Workflow InstanceID: {0} Workflow instance state: {1}",
record.InstanceId, workflowInstanceRecord.State);
sw.WriteLine("\n");
}
ActivityStateRecord activityStateRecord = record as ActivityStateRecord;
if (activityStateRecord != null)
{
IDictionary variables = activityStateRecord.Variables;
StringBuilder vars = new StringBuilder();
if (variables.Count > 0)
{
vars.AppendLine("\n\tVariables:");
foreach (KeyValuePair variable in variables)
{
vars.AppendLine(String.Format(
"\t\tName: {0} Value: {1}", variable.Key, variable.Value));
}
}
sw.WriteLine("------ActivityStateRecord------");
sw.WriteLine("Activity DisplayName: {0} :ActivityInstanceState: {1} {2}",
activityStateRecord.Activity.Name, activityStateRecord.State,
((variables.Count > 0) ? vars.ToString() : String.Empty));
sw.WriteLine("\n");
}
CustomTrackingRecord customTrackingRecord = record as CustomTrackingRecord;
if ((customTrackingRecord != null) && (customTrackingRecord.Data.Count > 0))
{
sw.WriteLine("------CustomTrackingRecord------");
sw.WriteLine("\n\tUser Data:");
foreach (string data in customTrackingRecord.Data.Keys)
{
sw.WriteLine(" \t\t {0} : {1}", data, customTrackingRecord.Data[data]);
}
sw.WriteLine("\n");
}
}
}
}
让我解释一下:简而言之,当您处理 WF 4.0 跟踪时,您必须理解三个概念
- 跟踪记录:这些是您的工作流发出的信息记录。有四个派生类对应于四种类型的跟踪记录
WorkflowInstanceQuery
:关于工作流实例状态的事件。例如:已启动、已挂起、已卸载等…ActivityStateQuery
:关于 WF 内活动的事件。CustomTrackingQuery
:您想要跟踪的任何自定义信息。BookmarkResumptionQuery
:您想要跟踪的、当该书签恢复时(书签将在后面介绍)的书签名称。- 跟踪配置文件:充当跟踪记录的过滤器,以便只跟踪所需信息。
- 跟踪参与者:用于写入跟踪信息的媒介。WF 4.0;附带一个写入 ETW(Windows 事件跟踪)的参与者。可以轻松开发自定义参与者,例如将跟踪数据写入 SQL Server,或者像本示例一样,写入文件系统。
在我的 C# 代码中,我创建了TrackingProfile
类的一个实例,并添加了三种类型的记录:WorkflowInstanceQuery
、ActivityStateQuery
和CustomTrackingQuery
。
对于WorkflowInstanceQuery
,我要求配置文件使用“*”运算符跟踪所有工作流状态。我也可以限制我想跟踪的状态数量(查看注释掉的代码)。请记住,在我的工作流中,我有持久化和卸载状态(查看行为配置和延迟时间),因此您将在跟踪数据中看到这些状态。对于 ActivityS
tateQuery,我要求配置文件跟踪所有活动的全部状态以及这些活动声明的任何变量。同样,对于CustomTrackingQuery
,我要求配置文件跟踪我定义的任何自定义数据(请记住,我在自定义活动中定义了自定义数据)。
现在我已经定义了跟踪记录和跟踪配置文件,最后一步是定义跟踪参与者。我本可以使用开箱即用的EtwTrackingParticipant
类,该类写入 ETW,但为了使事情更有趣,我创建了一个自定义跟踪参与者类(FileTrackingParticipant
),它派生自TrackingParticipant
。请查看该类的 C# 代码,它只是获取每个跟踪记录的信息并将其写入一个文件。
回到主程序,我将我的跟踪配置文件(fileTrackingProfile
)与我的跟踪参与者(fileTrackingParticipant
)关联起来,最后将参与者添加到主机的 WorkflowExtensions 集合中。
为确保您拥有运行程序,以下是简单的客户端程序 C# 代码
static void Main(string[] args)
{
ServiceReference1.ServiceClient proxy = new ServiceReference1.ServiceClient();
Guid instanceid = (Guid)proxy.Operation1();
Console.WriteLine("Client Done");
Console.ReadLine();
}
运行服务,使其监听“https://:8089/TestWF”的请求;现在运行客户端控制台,并等待服务控制台窗口打印“Workflow Ended”。现在检查写入跟踪的文件,您将看到工作流事件、活动事件、变量和自定义信息,全部已跟踪… 请参见下图(为便于展示已重新格式化)
WF 4.0 书签
书签用于标记工作流中您希望它等待某个事件发生(例如获取输入)的位置。在此,您希望 WF 停止执行并释放工作线程。因此,书签是在代码活动内部创建并命名的一个恢复点;一段独立的 C# 代码(通常是主机)知道这个命名点,并在某个时候使用它来传递数据并恢复 WF。正如您可能猜到的,我们还将为 WF 实例配置持久化,以便使用书签正确地进行持久化和恢复…
再一次,让我们创建一个工作流控制台应用程序,并使用生成的“Workflow1.xaml”文件,构建以下流程图工作流
它很简单:一旦 WF 启动,它会打印实例的线程 ID,然后显示一条消息提示用户输入产品名称;然后执行一个名为“SubmitOrderName
”的自定义活动。此活动创建一个书签,WF 将在此等待用户输入产品名称。接下来——在 WF 恢复后——再次打印执行的线程 ID,最后打印消息“Workflow Ended”。
让我们看一下SubmitOrderName
活动
public sealed class SubmitOrderName : NativeActivity
{
protected override bool CanInduceIdle
{
get
{
return true;
}
}
protected override void Execute(NativeActivityContext context)
{
context.CreateBookmark("OrderNameBookmark",
new BookmarkCallback(OnBookmarkCallback));
}
void OnBookmarkCallback(NativeActivityContext context,
Bookmark bookmark, object val)
{
Console.WriteLine("Order Name is {0}", (string)val);
}
}
它派生自NativeActivity
并重写CanInduceIdle
属性以返回 true,这意味着此活动会导致工作流进入空闲状态。Execute
方法创建书签并将其命名为“OrderNameBookmark
”,同时定义书签回调——当书签被恢复时(由主机,稍后我们将看到)将执行的代码。
最后,定义了书签回调方法,它只是在控制台中显示用户输入。
现在,当我们研究主机的 C# 代码时,完整的场景将变得清晰。打开Program.cs并观察代码。我将分块解释,以便更容易理解。第一部分如下所示
static AutoResetEvent instanceUnloaded = new AutoResetEvent(false);
static Guid id;
AutoResetEvent
允许需要访问资源的线程通过信号进行通信。一个线程通过在AutoResetEvent
上调用WaitOne
来等待信号。如果AutoResetEvent
处于非信号状态(线程通过调用Set
在AutoResetEvent
上进入信号状态),则该线程将阻塞,等待当前控制资源的线程通过调用Set
来发出资源可用的信号。
那么,为什么我们的示例需要这种线程管理?很简单,因为在 WF 中,主机(在此示例中为WorkflowApplication
)在一个线程上工作,而工作流实例将在另一个线程上启动(这就是我在示例中打印线程 ID 的原因…)。那么,当创建书签并随后将控制权切换回主机以提交输入并恢复书签时,我们如何协调主机和实例本身的工作呢?答案是使用AutoResetEvent
… 如果这一点还不清楚,请放心,我稍后将详细解释…
static void Main(string[] args)
{
WorkflowApplication app = new WorkflowApplication(new Workflow1());
//setup persistence
InstanceStore store = new SqlWorkflowInstanceStore(
@"Data Source=.\SQL2008;Initial Catalog=" +
@"SqlWorkflowInstanceStore;Integrated Security=True");
InstanceHandle handle = store.CreateInstanceHandle();
InstanceView view = store.Execute(handle,
new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
handle.Free();
store.DefaultInstanceOwner = view.InstanceOwner;
app.InstanceStore = store;
接下来,我们使用WorkflowApplication
来托管我们的工作流。在之前的示例中,我使用了WorkflowServiceHost
来托管我的 WF 服务;这里,我使用的是WorkflowApplication
,因为我没有使用通信形状。
接下来,我设置持久化存储并将其附加到我的应用程序主机。
app.PersistableIdle = (e) =>
{
return PersistableIdleAction.Unload;
};
app.Unloaded = (workflowApplicationEventArgs) =>
{
Console.WriteLine("WorkflowApplication has Unloaded\n");
instanceUnloaded.Set();
};
接下来,我设置了应用程序主机的两个属性:PersistableIdle
和Unloaded
。
PersistableIdle
设置当工作流实例空闲且可以持久化时要调用的操作。可能的操作包括PersistableIdleAction
枚举值之一:None
(什么也不发生)、Persist
(持久化但保持实例执行)或Unload
(持久化并从内存中卸载)。在我的例子中,我指定实例将被卸载。
注意:当调度程序没有更多待处理的工作项并且工作流运行时可以持久化时,将调用PersistableIdle
函数。在我们的示例中,一旦我们创建书签,就会发生这种情况,我们稍后将看到。
Unloaded
设置当我的工作流实例达到卸载生命周期事件时要调用的操作。请记住,我们刚刚通过PersistableIdle
属性指示我们的工作流被卸载。在这里,我们说明一旦实例被卸载将执行什么 C# 代码。我们只是向控制台打印一条消息,然后调用Set
on the AutoResetEvent
:这意味着执行我工作流的线程已发出信号,表示它已空闲,并且可以将控制权交给另一个线程(在这种情况下为主机线程)。
id = app.Id;
app.Run();
Console.WriteLine("Host thread: " +
System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
instanceUnloaded.WaitOne();
接下来,我提取我工作流的 GUID——稍后将用于恢复工作流——然后调用主机的Run
方法来启动工作流执行。但等等,这里有一个问题:尽管我调用了Run
,工作流直到我调用WaitOne()
on AutoResetEvent
才会开始执行。为什么?因为这标志着主机线程需要控制权,并且现在正在等待获取它(当AutoResetEvent
处于信号状态时)。
//resume
string name = Console.ReadLine();
app = new WorkflowApplication(new Workflow1());
app.InstanceStore = store;
app.Completed = (workflowApplicationCompletedEventArgs) =>
{
Console.WriteLine("\nWorkflowApplication has Completed in the {0} state.",
workflowApplicationCompletedEventArgs.CompletionState);
};
app.Unloaded = (workflowApplicationEventArgs) =>
{
Console.WriteLine("WorkflowApplication has Unloaded\n");
instanceUnloaded.Set();
};
app.Load(id);
app.ResumeBookmark("OrderNameBookmark", name);
Console.WriteLine("Host thread: " +
System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
instanceUnloaded.WaitOne();
Console.ReadLine();
最后一部分 C# 代码是工作流恢复。正如我们将看到的,当我们运行工作流时,如前面的 C# 代码所述,在此阶段,工作流已执行,并且创建书签的活动已被执行,工作流进入空闲状态等待用户输入。因此,工作流已从内存中卸载,并且主机线程再次获得控制权。
所以现在主机正在等待用户输入。一旦获得输入,我们就会重新初始化我们工作流的主机和持久化存储。接下来,我们设置其Completed
和Unloaded
操作。请注意,在Unloaded
操作中,我们再次调用Set
on the AutoResetEvent
,这意味着线程已完成其工作…
最后,我们使用正确的 ID 实际加载实例并实际恢复工作流。
让我们运行程序并实际查看
在第一行设置一个断点。继续执行代码,直到到达“instanceUnloaded.WaitOne()
”行,如下所示
总之,直到此时,我们已经初始化了工作流,设置了持久化,设置了(显然尚未执行)PersistableIdle
和Unloaded
操作,最后运行了工作流。但是,请检查控制台窗口,您将看到打印的唯一消息是主机线程 ID,这是由主机执行的,如上面的最后一行所示;这表明工作流尚未开始执行,尽管我们已调用Run
方法。为什么又是这样?因为我们还没有调用WaitOne
on AutoResetEvent
来发出信号,主机可以释放线程,并且可以进行工作流实例的执行。
现在按 F10。工作流将立即开始执行。您可以通过观察控制台中打印的两个额外消息来验证这一点。此外,我在自定义活动中设置的一个断点将被命中,表明执行已达到此处,如下所示
按 F10。现在将创建书签。您能猜到接下来会发生什么吗?对了!PersistableIdle
操作将被执行,因为一旦创建了书签,就表示工作流可以持久化。接下来,Unloaded
操作将被执行,因为我们选择了PersistableIdleAction.Unload
… 如下图所示
一旦Unloaded
操作通过调用Set
方法完成执行(如上所示),执行控制权将移回主机应用程序,您可以通过观察另一个设置在恢复 C# 代码上的断点来看到这一点,如下所示
现在,在恢复执行之前,让我们验证持久化是否正常工作。毕竟,创建书签和卸载工作流实例应该意味着它现在已持久化到数据库中。打开您的 SQL Server 并观察“InstancesTable”表,您将看到一个对应于持久化工作流实例的记录。
继续:执行恢复 C# 代码
首先,您将被提示输入。然后将使用实例 GUID 重新加载工作流实例,并将调用ResumeBookmark
。这将导致OnBookmarkCallback
方法被执行,最后Completed
和Unloaded
操作将被执行。
最终的控制台输出应如下所示
下一步?
我已经在撰写一篇关于 Windows Server AppFabric 的完整文章。在这篇文章中,所有示例都使用了自托管;在我的下一篇文章中,我将使用 AppFabric 来托管 IIS 7。
我已经在我的博客上发布了关于 AppFabric 的视频录制(点击此处),这些视频是我上次社区会议的录制内容;但我正在撰写一篇带有源代码的文章,以便更方便地参考…