This is the second post about Workflow Foundation 4, Beta 2. This time, it’s about native activities and custom extensions. The sample used here is the same as in my last post (Persistence in Workflow Foundation 4). In that post, I used a simple workflow which incremented a integer value by one each time it was executed. To make things more interesting, the custom activity does this in an asynchronous manner.
The activity is a rather simple one, since it uses an extension to do the actual work:
1: using System.Activities;
2:
3: namespace WorkflowConsoleApplication1
4: {
5: public class IncrementActivity : NativeActivity
6: {
7: public OutArgument<int> Result { get; set; }
8:
9: protected override bool CanInduceIdle { get { return true; } }
10:
11: protected override void Execute(NativeActivityContext context)
12: {
13: var extension = context.GetExtension<IncrementExtension>();
14:
15: var bookmark = context.CreateBookmark("test", IncrementCallback);
16: extension.Increment(bookmark);
17: }
18:
19: private void IncrementCallback(NativeActivityContext context, Bookmark bookmark, object value)
20: {
21: Result.Set(context, (int)value);
22: }
23: }
24: }
The action has an out argument, the Result property. The value written to this argument is incremented by one each time the activity is called. The key to enable automatic workflow persistence are bookmarks. A short summary about bookmarks can be found in plenty of blogs (for example here). But most of them are wrong with their examples: The syntax has changed quite a bit when it comes to how to create bookmarks. When an activity calls the CreateBookmark method, the runtime automatically stops the workflow instance at that point and persists it to the instance store, if configured. The bookmark takes a name and a callback. The workflow instance is resumed, when the callback is called. In this example, the callback function sets the ‘Result’ argument to the value it receives from the extension.
Note that the activity needs to return true in the CanInduceIdle property. Otherwise, a call to CreateBookmark method will throw an exception.
Once the bookmark has been created, it is handed over to the IncrementExtension.
The IncrementExtension has one method that actually does something: The Increment method. It takes a Bookmark instance as input and resumes it after waiting for 1 second on a different thread:
1: using System;
2: using System.Activities;
3: using System.Activities.Hosting;
4: using System.Collections.Generic;
5: using System.Threading;
6:
7: namespace WorkflowConsoleApplication1
8: {
9: class IncrementExtension : IWorkflowInstanceExtension
10: {
11: int _Index;
12: private WorkflowInstance _Instance;
13: Bookmark _Bookmark;
14:
15: public void Increment(Bookmark bookmark)
16: {
17: ThreadPool.QueueUserWorkItem(state =>
18: {
19: Thread.Sleep(1000);
20: var value = Interlocked.Increment(ref _Index);
21: _Bookmark = bookmark;
22: ContinueWorkflow(value);
23: }, null);
24: }
25:
26: private void ContinueWorkflow(int value)
27: {
28: _Instance.BeginResumeBookmark(_Bookmark, value, CompleteResume, null);
29: }
30:
31: public void CompleteResume(IAsyncResult ar)
32: {
33: var result = _Instance.EndResumeBookmark(ar);
34: }
35:
36: IEnumerable<object> IWorkflowInstanceExtension.GetAdditionalExtensions()
37: {
38: yield break;
39: }
40:
41: void IWorkflowInstanceExtension.SetInstance(WorkflowInstance instance)
42: {
43: _Instance = instance;
44: }
45: }
46: }
To resume the workflow instance, the extension needs to know which instance it should resume. By default, it does not have this info. It only has an instance of a Bookmark. To get the workflow instance associated with an instance of an extension instance, the extension needs to implement the IWorkflowInstanceExtension. This method is called during the initialization of the extension instance.
Given the instance and the bookmark, the extension can now call the BeginResumeBookmark method on the workflow instance.
One question remains: Where does the extension instance come from? Simple, it is provided during the initialization of the workflow:
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: var app = new WorkflowApplication(new Workflow1());
6:
7: // Specify that the workflow should be persisted everytime it is idle.
8: app.PersistableIdle = e => PersistableIdleAction.Persist;
9:
10: // Add an instance store that is used to persist the workflow.
11: app.InstanceStore = new SqlWorkflowInstanceStore("Data Source=(local);Initial Catalog=WorkflowDemo;Integrated Security=True");
12:
13: app.Extensions.Add(() => new IncrementExtension());
14: app.Load(new Guid("1E1A5265-A135-4D42-9D64-2ECCCF22E888"));
15: app.Run();
16:
17: Console.WriteLine("Current Instance id: {0}", app.Id);
18:
19: Console.ReadLine();
20:
21: app.Unload();
22: }
23: }
The extension is added to the workflow application in line 13. The app.Extensions.Add method either takes a singleton instance of the extension (which is then used each time the extension is requested) or a factory method which creates a new instance each time the extension is requested.