Spawned Contexts - Replicator, While, State, EventHandlers, and CAG

Ever wonder why this.delayActivity1.TimeoutDuration sometimes doesn't change the timeout duration? How come this.callExternalMethodActivity1.ParameterBindings["(ReturnValue)"] isn't giving you the value you expect in some scenarios? How is it possible that sometimes this.GetActivityByName("foo") does not equal my sender for one of foo's events? 

The answer to all of these questions is: spawned contexts.

One of the most powerful and most easily misunderstood concepts in Windows Workflow Foundation (WF) is that of new contexts executing cloned activities. Before we even define a spawned context, let's go back to the beginning ...

The Beginning

One of the binding qualities of the activity state transition diagram is that there are no transitions from Closed back to Executing. You can only get to Executing through an Initialized activity and you can only get to Initialized from a brand new instance. 

How do we handle looping activities then? Replicator, While, and CAG all give the impression of executing the same activity (or set of activities) multiple times. The answer is by creating a new context and cloning the template activity (explained later).

ActivityExecutionContext Interlude

First, let's fully understand the ActivityExecutionContext. This object is passed to all scheduled calls either as a specific parameter (Execute, Cancel, HandleFault) or as the sender (QueueItemAvailable handler, StatusChanged handler). The ActivityExecutionContext provides the activity writer with an interface for controling activity execution (hence the first two words in the name) while giving the runtime enough control to enforce the rules of the WF engine.

But what about the "Context" part of the name? A context in WF is a sphere of execution. There is a root activity for each context and only activities which exist in that context can be executed in that context. In short, a context is a mechanism used by the runtime to determine on which set of activities to enforce rules.

One important note is that the ActivityExecutionContext is merely a short-lived expression of the underlying context - every scheduled call to an activity method gets a new instance of the ActivityExecutionContext object which has been configured specifically for that activity. You'll notice, however, that the Guid associated with the context does not change.

Cloning Activities

Whenever a single activity needs to be executed multiple times it must be cloned. Contexts are the mechanism provided to the activity writer for making this happen. The code looks like this:

ActivityExecutionContext childContext = currentContext.ExecutionContextManager.CreateExecutionContext(childActivity);

This code will cause a new context, childContext, to be created with a root activity which is a clone of childActivity. Note that this is a deep cloning so if childActivity is a composite activity then its entire tree is cloned as well. Consider that we have a custom activity called WorkflowRoot which clones its only child activity using the above code. Visually, we now have the following tree of contexts:

RootContext
| WorkflowRoot (1)
| childActivity (1)
| grandChildActivity (1)
- childContext
childActivity (2)
grandChildActivity (2)

What?

Looking at the above diagram there are several questions which come up. Let's try to deal with a couple of easy ones first:

  • Are changes to childActivity(1) or childActivity(2) reflected in the other instance?
    No. Once cloned these instances have no connection. Changes to the template will affect future clones and changes to the clone will affect its own execution, but changes to one will not affect the other.
  • What is the return value of childActivity(2).Parent?
    WorkflowRoot(1). The activities inside a new context do not know that they are not part of the rest of the tree. The Parent property of the context's root activity still points to the original parent. It is only when walking down the tree that the context's are noticeable. For example, WorkflowRoot(1).Activities[0] will always return childActivity(1) and never childActivity(2). Said another way, childActivity(2).Parent.Activities[0] == childActivity(1). This is strange at first glance, but this soon becomes natural.

Scenarios

While Loop with Delay

Consider the following workflow:

WhileActivity
DelayActivity

Not very useful, I'll admit, but it is handy for this demonstration. Now, if we have implemented this as a code only workflow, we'll probably have some field defined on our root called delayActivity1. Let's say that we want to change the delay amount each time through the loop, so we subscribe to the InitializeTimeoutDuration event with the following code:

// WRONG CODE
this.delayActivity1.TimeoutDuration = TimeSpan.FromSeconds(iterationCount);

Assuming iterationCount is a variable that is incremented each time through the loop, we expect to see: delay 1 second, delay 2 seconds, delay 3 seconds, etc. This, however, is not what we see. Instead we get: delay 0 seconds, delay 1 seconds, delay 2 seconds, etc. 

The reason is that the WhileActivity is spawning a new context when it executes the child. So, each iteration looks like this:

RootContext
| WhileActivity(1)
| Delay(1)
- childContext
Delay(1 + iterationCount)

this.delayActivity1 ALWAYS refers to Delay(1) and therefore we are updating the template every time InitializeTimeoutDuration is called. That means we are always one timeout amount behind ... Delay(2) is about to execute with TimeoutDuration set to 0 seconds and we update the template to 1 seconds. Delay(3) is created with a 1 second timeout because it is just a clone of the template at that point in time.

Some new code for InitializeTimeoutDuration:

// RIGHT CODE
((DelayActivity)sender).TimeoutDuration = TimeSpan.FromSeconds(iterationCount);

This time we will see the following: delay 1 seconds, delay 2 seconds, delay 3 seconds, etc. Here we have updated the cloned value instead of the template. Note that for ALL events subscribed to in code beside the sender will be the actual instance of the activity which is currently running. 

Therefore, the sender above will always be the right one even if the delay is not in a context spawning activity. If you want to avoid issues, learn to access activity properties in a context safe way (like using the sender objects) so that you do it right when it counts. If the delay weren't in a context spawning activity then the WRONG CODE and the RIGHT CODE would be equivalent, but if the delay is in a context spawning activity then the WRONG CODE will never work.

Replicator and GetActivityByName

Replicator
Sequence
CallExternalMethodActivity
HandleExternalEventActivity

The above workflow is a common pattern for replicated user tasks. The CallExternalMethodActivity notifies the user of the task and the HandleExternalEventActivity gets an event when the task is complete. Let's say that we're going to assign 3 tasks for UserA, UserB, and UserC so our replicator will initailize itself with the collection {"UserA", "UserB", "UserC"}. Assuming that the user name is the correlation parameter, our ChildInitialized handler might look like:

// WRONG CODE
CallExternalMethodActiivty act = this.GetActivityByName("createTask1") as CallExternalMethodActivity;
act.ParameterBindings["userName"].Value = e.InstanceData;

The code above will not work as expected. Let's look at why by examining the contexts created:

RootContext
| Replicator (1)
| Sequence(1)
| CallExternalMethodActivity(1)
| HandleExternalEventActivity(1)
- childContext1 (e.InstanceData = "UserA")
| Sequence(2)
| CallExternalMethodActivity(2)
| HandleExternalEventActivity(2)
- childContext2 (e.InstanceData = "UserB")
| Sequence(3)
| CallExternalMethodActivity(3)
| HandleExternalEventActivity(3)
- childContext3 (e.InstanceData = "UserC")
Sequence(4)
CallExternalMethodActivity(4)
HandleExternalEventActivity(4)

"this" in our code snippet refers to the root workflow which exists in the RootContext. When we call GetActivityByName and pass the CallExternalMethodActivity's name we will get the instance that is in the root context - CallExternalMethodActivity(1). What we want is the one in the current context so the code should look like:

// RIGHT CODE
CallExternalMethodActiivty act = e.Activity.GetActivityByName("createTask1", true) as CallExternalMethodActivity;
act.ParameterBindings["userName"].Value = e.InstanceData;

Note the two changes - first we use e.Activity instead of this. e.Activity is the clone of the replicator's template (Sequence(2-4)). Second, we have passed the parameter true to GetActivityByName. This tells the method to look only in the context of the activity on which it was called. This keeps the method from walking into other parts of the tree and returning the RootContext instance.

Conclusion

Hopefully this post eases some confusion around contexts and doesn't make it worse. Please post comments if you want clarifications on anything written above or if you want more information about one topic or another. I will write a separate entry at some point to discuss how to manage contexts you create in custom activities.