Workflow Scopes and Execution Properties
or, Workflow Execution Properties for custom data passing.
I wrote this after seeing a few forum posts from the Workflow Foundation Beta Forum on the same basic theme. Workflow 4.0 Arguments and Variables certainly do work for passing data to and from Activities in your workflow - but sometimes they don't feel easy enough to use.
As an example, you might have created many custom activities that all need the exact same InArgument, such as a database connection, or a file writer. When you build your workflow manually in Workflow Designer, you notice that you're setting the exact same InArgument on every activity in your workflow.
For scenarios like this, it would be nice if your custom activity was able to configure itself automatically based upon the scenario or context in which it executes. Then the customer designing the workflow would have less work to do configuring the workflow, there would be less variables to look at in the workflow designer, and there would be 'more happy'.
Actually, during design of the framework there were a few activities that came close to the above description. I'm actually thinking about the messaging activities. One feature added to the messagin activities was the ability to use a scope (which is really just a special Activity) to make a CorrelationHandle implicitly available as configuration to other activities without setting it as an InArgument. So the CorrelationScope activity was invented. CorrelationScope takes a CorrelationHandle and makes that handle available to all its descendant messaging activities - without setting it in the InArguments on those activities.
Obvious questions
- 'How does it work?'
- 'Fine, but I'm not sure what a CorrelationHandle is or how I would use CorrelationScope, how about a simpler example?'
CorrelationScope works by using ExecutionProperties. The key feature of an ExecutionProperty that we use is that they allow you to share data 'globally' with their descendant activities - and only their descendant activities (in-scope activities). Let's look at an example, where we want to share an FileStream between activity FileScope and all of its descendant activities FileWriter.
First, let's implement class FileScope. It will need to subclass NativeActivity in order to get full access to execution properties (through the NativeActivityContext). It looks like this:
public sealed class FileScope : NativeActivity
{
public InArgument<FileStream> Val { get; set; }
public Activity Body { get; set; }
protected override void Execute(NativeActivityContext context)
{
FileStream f = Val.Get(context);
context.Properties.Add("TheFile", f);
context.ScheduleActivity(Body);
}
}
We get the file from an InArgument, then we store it as an execution property named "TheFile".
By the way, we will want to create a simple custom activity designer so that we can add children to FileScope in workflow designer. Here's a quick and easy activity designer. Add a new ActivityDesigner item to the project. Inside the <Grid> tags, we add a WorkflowItemPresenter.
<sap:ActivityDesigner x:Class="WorkflowConsoleApplication2.FileScopeDesigner"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation">
<Grid>
<sap:WorkflowItemPresenter Item="{Binding Path=ModelItem.Body}" HintText="Body"/>
</Grid>
</sap:ActivityDesigner>
Of course, we need to associate the designer to our activity class before the designer will show up. If all our files are in one project then adding an attribute like this on class FileScope should work. (Namespaces should match).
[System.ComponentModel.Designer(typeof(WorkflowConsoleApplication2.FileScopeDesigner))]
public sealed class FileScope : NativeActivity ...
Now for the activity which will use the scope. Again we must subclass NativeActivity in order to access the execution property.
public sealed class FileWriter : NativeActivity
{
protected override void Execute(NativeActivityContext context)
{
FileStream f = (FileStream)context.Properties.Find("TheFile");
(new StreamWriter(f)).WriteLine("Hello File");
Console.WriteLine("f: {0}", f);
}
}
There is no InArgument in class FileWriter - which was the whole point. We can have ten FileWriters in one scope. All of them will write to the same file, and none of them will need an InArgument.
Any bugs in this code? You might have noticed that when I added an execution property I forgot something: I forgot to clean up the execution property after we were done with it. Well, here's some good news! It turns out that the framework will clean up our execution properties for us automatically, keeping them nicely scoped to the lifetime of our scope activity's execution.
What the framework doesn't know is that we might want to close the file! We could do such cleanup inside a completion callback - by calling a different overload of context.ScheduleActivity(), one with a CompletionCallback parameter.
For a sample using completion callbacks, check out the Entity Framework sample activities (for VS Beta2 it is under: WF\Scenario\ActivityLibrary\EntityActivities) and you will see the completion callback used in ObjectContextScope to enforce saving changes back to the database and proper disposal of the ObjectContextScope object created in ObjectContextScope.Execute().
One more question
- 'We are using ExecutionProperties. But I notice that IExecutionProperty wasn't involved in this example at all. Why not?'
Consulting MSDN, IExecutionProperty, according to the beta documentation,
'provides execution properties with a mechanism for configuring thread local storage before and after the work items of the associated activity'
Thread-local storage is an idea of global data, per-thread. To give a ridiculous example, you could use it to have a 'global' counter which counts how many Foo activities have executed on a particular thread. In reality it's more likely to be used for figuring out things like 'which Bar is the current Bar on this thread?'
Also, in MSDN note the function parameters of ExecutionProperties.Add are (string, object) . Reading between the lines, object means that implementing IExecutionProperty is optional - something you only need to do if you have some special to do and you want the runtime to call you back so that you can do it.
The bit in italics is key for understanding the 'basic' ExecutionScope sample (for Beta2 under: WF\Basic\CustomActivities\Code-Bodied\ExecutionProperties). IExecutionProperty is being used here not for passing data to other activities, but for a completely different purpose, which is to set/reset the Console color (which is thread-local data - which color is the current color for this thread?).
You can read more about the IExecutionProperty sample here on MSDN.
Finally, a little question. Why does CorrelationScope take an InArgument of CorrelationHandle instead of creating a CorrelationHandle automatically? Well I don't know, but maybe it could be more flexible that way...
Links
Now that you've read the blog post, go and play with the samples: download WCF and WF Beta 2 samples
[Last Revision 01/29/2010]