I am by no means an expert on Workflow. But after e-mailing back and forth with Jim Bears and Dave McCarter about a possible talk for the July meeting of the San Diego Developers Group, we concluded that not many people understand, use, or appreciate workflow. This could be due to bad experiences they have had in prior versions. Since the 4.0 version has addressed a lot of those shortcomings, we decided that it would be a great topic for discussion at a user group.
As per usual, I was too busy to prepare for the talk more than a day in advance. But I put together some slides, and formed a pretty good idea of the demo I wanted to do before I went in.
The first hour of the talk went pretty well. It was when I got into the unrehearsed part of the demo that things started to go awry. I had packaged up the first demo into a custom activity, and was trying to reuse that in a flowchart activity. Also I was switching from a WorkflowInvoker to running the workflow from a WorkflowApplication. At the same time I was adding persistence. I noted that I was still passing in the custom activity and not the workflow that contained the Flowchart, but I knew that something else was going on. I eventually found it (I was forgetting to call Run on the WorkflowApplication). Once I fixed that however, I forgot to go back and switch to the outer activity. That’s was the reason that nothing else was working.
This morning before I went to work, I fixed that problem, and corrected a couple of typos in the slides. Now that I am back home, I am going to finish the rest of the demo. That was always my plan, because I didn’t think that I would have time to do everything live.
I had already created one event – the completed event. But now that I allow the workflow to persist I want to capture another event – the PeristableIdle event
Here is an example of hooking that up:
app.PersistableIdle = e => { Console.WriteLine("Persisting..."); _persistingEvent.Set(); return PersistableIdleAction.Persist; };
then I want to change my main from this:
WorkflowApplication app = CreateNewWorkflow(); app.Run(); _completedEvent.WaitOne();
to this:
WorkflowApplication app = CreateNewWorkflow(); app.Run(); _persistingEvent.WaitOne(); app.ResumeBookmark("readPrizeCode", Console.ReadLine()); _completedEvent.WaitOne();
Run it again, and everything works, but now what happens if I close the console application after receiving the prize code?
I need someway of loading the existing workflow, but I don’t have anything that I can load it by. As I mentioned last night there are a couple of ways to do this:
1) by using Promotable properties so that some of your properties are persisted along with the workflow.
2) Just tracking the mapping between your custom property and the instance ID from another table
I chose the second option, and added in the DataAccessLayer to do this already, but to take advantage of this feature I need to add the mapping record when it goes idle and take out the mapping when it completes. The final result looks something like this:
static readonly ManualResetEvent _completedEvent = new ManualResetEvent(false); static readonly ManualResetEvent _persistingEvent = new ManualResetEvent(false); static readonly ManualResetEvent _unloadedEvent = new ManualResetEvent(false); static void Main(string[] args) { string email = null; while (string.IsNullOrWhiteSpace(email)) { Console.WriteLine("Enter your e-mail address:"); email = Console.ReadLine(); } bool done = false; while (!done) { WorkflowApplication app = CreateNewWorkflow(email); Guid instanceId; using (var session = new EfSession()) { instanceId = session.Workflows.GetWorkflowInstance(email); } if (instanceId != Guid.Empty) { app.Load(instanceId); string prizeCode = null; while (string.IsNullOrWhiteSpace(prizeCode)) { Console.WriteLine("Enter your prize code:"); prizeCode = Console.ReadLine(); } app.ResumeBookmark("readPrizeCode", prizeCode); _completedEvent.WaitOne(); done = true; } else { app.Run(); _unloadedEvent.WaitOne(); } } } public static WorkflowApplication CreateNewWorkflow(string email) { IDictionary<string, object> outputs = null; var app = new WorkflowApplication( new QuestionForPrize()); app.Completed += e => { outputs = e.Outputs; Console.WriteLine("Removing instance {0}...", app.Id); using (var session = new EfSession()) { var instances = from wi in session.Workflows.All where wi.EmailAddress == email && wi.WorkflowInstanceId == app.Id select wi; var instance = instances.SingleOrDefault(); if (instance != null) { session.Workflows.Delete(instance); session.Save(); } } _completedEvent.Set(); }; app.PersistableIdle = e => { Console.WriteLine("Persisting instance {0}...", app.Id); var wi = new WorkflowInstance { EmailAddress = email, WorkflowInstanceId = app.Id, }; using (var session = new EfSession()) { session.Workflows.Add(wi); session.Save(); } _persistingEvent.Set(); return PersistableIdleAction.Unload; }; app.Unloaded = e => { Console.WriteLine("Instance {0} has been unloaded", app.Id); _unloadedEvent.Set(); }; app.InstanceStore = GetInstanceStore(); return app; } private static InstanceStore GetInstanceStore() { var instanceStore = new SqlWorkflowInstanceStore( ConfigurationManager.ConnectionStrings["Workflow"].ConnectionString) { HostLockRenewalPeriod = TimeSpan.FromSeconds(1) }; InstanceHandle handle = instanceStore.CreateInstanceHandle(); InstanceView view = instanceStore.Execute( handle, new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30)); handle.Free(); instanceStore.DefaultInstanceOwner = view.InstanceOwner; return instanceStore; }
Here are the slides and demos for the talk. Thanks everyone for coming!