Download the sample code for this article here.
Workflow execution properties are a necessary feature of Windows Communication Foundation (WCF) services, but they're just not that well documented. I'll try to give you some perspective on when and how to leverage them within your custom activities. Why should you consider using workflow execution properties for your workflows or workflow services? Because of the following closely related task scenarios:
- Scenario 1: You want to create an ambient variable that's available to all activities within a scope that is defined by your composite activity, but you don't want to have to manually wire up child activity arguments to use it.
- Scenario 2: You want to share data in a thread in an agnostic manner.
- Scenario 3: You want to enable use of the CLR or existing third-party libraries that use thread local storage (TLS) for key data.
The Problem with Thread Affinity in Workflow
Workflow execution properties enable you to achieve your goal in all three of these task scenarios. But to understand why they are important, you first have to understand the problem of thread affinity in workflow. This will also help you understand why scenarios 2 and 3 might be relevant to you.
Why is thread affinity a problem? By definition, workflows are hierarchically scoped trees of activities that execute in a thread-agnostic environment. Simply put, the workflow runtime assumes that an activity has no thread affinity and that it is, therefore, free to execute an activity on any thread of its choosing. Naturally, this means that if you store data within the thread, your activity may actually end up executing on a thread in which that data either isn't available or was set by a different activity instance.
The crux of the thread affinity problem centers on TLS. This is a mechanism provided by the CLR. It enables threads to store private data that is not shared with other threads but that is available globally to all code running on that thread. TLS is different from static variables, which are available to all threads in a process.
Now, take a look at some non-workflow cases that you might be familiar with that use TLS. This will help you understand where in the process thread affinity comes into play and why TLS can be problematic in a workflow service.
Transactions are the first surprise to most. Whenever you access System.Transactions.Transaction.Current, you are accessing a transaction that is stored in TLS. Trying to use this transaction within a workflow directly is a non-starter because you could run into an activity conflict. This would occur if one activity creates the transaction on one thread and if another activity that requires the transaction is scheduled to run on a different thread that does not have the transaction. Even worse, an activity may run on a thread that has the wrong transaction. Imagine the subtle difficulty of debugging the reason that otherwise successful transactions are being randomly rolled back—seemingly by unrelated activities!
The next non-workflow area is Identity—specifically, any access to Thread.CurrentPrincipal, such as Thread.CurrentPrincipal.Identity, or calls to Thread.CurrentPrincipal.IsInRole(). Without special safeguards in place, you make find yourself wondering why your activities encounter a generic identity in place of the expected one.
Globalization and localization via Thread.CurrentCulture (which provides you with formatting for dates and numbers, calendar used, sort orders, and writing system options) and Thread.CurrentUICulture (which assists you with the loading of localized resources such as translations of strings shown in a UI) are also dependent on TLS because they are manipulated by the current thread.
Finally, you may have in a library existing class user-defined thread properties that you want to use in a custom activity. For example, you may have classes that have fields decorated by using the ThreadStatic attribute, or you may have implementations that make explicit use of named slots. In other words, any access that's performed through the current thread creates a thread affinity that can lead to subtle problems at runtime if your activity's code runs one thread on a new or different thread that does not have the expected data in TLS.
Understanding Workflow Execution Properties
The scheduler of the workflow runtime elects the thread on which to run an activity. So, ultimately, activities need a way to set up (when the activity starts executing) and tear down (when the activity is completed) any data that's maintained in TLS. In between execution and completion, there has to be a way to make sure that the data in TLS is available to the subtree of the currently executing activity. Workflow execution properties provide these required hooks.
Creating execution properties. The process of creating and using execution properties follows three general steps: Create the execution property class, create a scoping activity, and create child activities. You begin by creating a class that encapsulates the data that you want to be shared. This class should implement System.Activities.IExecutionProperty if you want to be able to set up anything that requires TLS on the runtime thread before the activity is executed on it. This is also true if you have to clean up TLS after it's completed. If you don't implement IExecutionProperty, your property will still be available to all children, but you can't use TLS storage. Figure 1 shows an example of a simple execution property class that implements IExecutionProperty and IPropertyRegistration.
Next, create a composite scoping activity that is derived from the NativeActivity class. Within the Execute override, register the execution property through the Properties property of the NativeActivityContext class. Notice that you do not have to clean up the execution properties because they are cleaned up automatically by runtime when they go out of scope. Figure 2 shows a scope activity that is used to register our execution property.
Finally, create child activities that are derived from the NativeActivity class and that retrieve the execution property from the NativeActivityContext class that's available within the Execute override. Figure 3 shows the implementation of a child activity that can access the execution property that's registered by the scope activity. Notice that because execution properties are added and accessed through NativeActivityContext, you can use only custom activities that are derived from NativeActivity to interact with execution properties.
Execution properties at runtime. Now that you understand the basics of creating execution properties, it's time to examine how they work at runtime. First, your scoping activity begins to execute. Within the Execute override, the activity then uses context.Properties.Add to register new execution properties. This defines the scope to be passed to the activity instance. If the added execution property implements IPropertyRegistrationCallback, the Register method is called on that property.
At some point, a work item that represents a child activity instance must be executed. The Execution Property Manager sets up the workflow thread. In turn, calls to SetupWorkflowThread are made on every instance of IExecutionProperty that's added. Then the ActivityExecutor executes the activity instance. If an error occurs during execution, the process skips ahead to the thread cleanup stage.
After the activity is completed, the Execution Property Manager cleans up the workflow thread. It does so by calling CleanupWorkflowThread on every instance of IExecutionProperty that's added. At this point, the execution of the scope activity is almost completed. If the added execution property implements IPropertyRegistrationCallback, the Unregister method is called on that property.
Timing of IExecutionProperty setup and cleanup calls. Now, take a more detailed look at IExecutionProperty. IExecutionProperty consists solely of two parameterless methods: SetupWorkflowThread and CleanupWorkflowThread. Both parameters are called in pairs multiple times throughout the scope activity's lifetime. For example, whenever the scope activity completes its Execute method, CleanupWorkflowThread is called.
Before a child activity runs, SetupWorkflowThread is called. After the child activity completes its Execute method, CleanupWorkflowThread is called. More generally, these two methods are called whenever the scheduler executes a work item within a scope that contains execution properties. In this manner, it ensures that if there's any chance that a new thread is used, that thread is set up and cleaned up correctly.
If your execution property is meant to access data that is typically stored in TLS, you must back up the current values in TLS within SetupWorkflowThread and overwrite these values by using the values of your property. Similarly, it is within CleanupWorkflowThread that you should restore the values that you backed up, so that any subsequent code that runs on that thread has the expected TLS state.
Timing of IPropertyRegistration Register and Unregister calls. IExecutionProperty is perfect if you want to set up and tear down state every time an activity executes. But what if you also want to run a final cleanup when the scope activity finishes and the execution property goes out of scope? To do this, implement IPropertyRegistrationCallback together with your execution property class. This amounts to defining the Register and Unregister methods of the execution property class.
Register is called when the scope activity adds the execution property to the execution properties collection, effectively when the execution property first goes into scope. Unregister is called automatically when the scope activity finishes, effectively when the workflow execution property goes out of scope.
Where WF uses execution properties. With an understanding of execution properties under your belt, it's probably a lot more interesting to look at the built-in activities that use them. Let's take a look at a few of them to see how execution properties were helpful in their implementation.
- TransactionScope uses an execution property to pass around a RuntimeTransactionHandle that manages and provides access to the ambient transaction in a thread-agnostic manner.
- TryCatch uses an execution property to make the caught exception and activity source available to Rethrow activities within its Catch blocks.
- Pick uses an execution property to manage its bookmarks and current state and to make them available to its Pick Branches.
- CompensableActivity uses an execution property to pass its CompensationToken to Compensate and Confirm activities.
Execution Properties, Workflow Services, and You
By now you may be asking where execution properties apply to workflow services. Let's begin by looking at a general category of types that are widely used in workflow services that make use of execution properties: handles.
Handles are classes that are derived from the abstract class System.Activities.Handle and that use workflow execution properties to manage the handles that are in scope. A handle typically takes on the type full name of the execution property that it consumes. Also, the scope at which the handle is defined also defines the scope for the execution property. There are four notable examples of handles that you may have already encountered in the process of building workflow services:
- CorrelationHandle is used to associate activities with an InstanceKey. More commonly, it is used to associate the workflow instance with a set of values that are received together with an input message. Internally, a correlation handle also registers a BookmarkScopeHandle to manage bookmarks for the service operations, and it registers a NoPersistHandle to make sure that the workflow cannot be persisted within the service operation. Correlation handles are used by activities such as Receive, Send, CorrelationScope, and CorrelationInitializer.
- RuntimeTransactionHandle is used by both the TransactionScope and TransactedReceiveScope to manage the ambient transaction. Notably, it implements Handle, IExecutionProperty, and IPropertyRegistrationCallback, and it serves as a sophisticated example of how to deliver thread-agnostic performance to items that otherwise exist in TLS.
Beyond these uses, there is one extensibility point for workflow services that you just can't get to any other way. This is when you want to get at the OperationContext class of a service operation.
Implementations of ISendMessageCallback and IReceiveMessageCallback are used to interact with the operation context. These implementations are registered with the runtime by creating a scope native activity that adds the implementation as an execution property, as we discussed previously. You can see a rich example of this implementation in the OperationContextScope activity of the Workflow Services Security Pack.
A Reliable Approach
Now that you are armed with the knowledge to avoid thread affinity issues in your workflow designs, go make use of execution properties in your custom activities. You'll make your services a little more predictable in their execution and simpler in their design.