Public and Private in Depth (“Custom ‘Activity Sequences’ with Private Implementation” Interlude)

[In this interlude, we’re spiritually continuing the Custom ‘Activity Sequences’ With Private Implementation series, but without referring to any code from the original problem.

Read Part 1 for the introduction. Or guess from this summary – we’re looking at how we can build custom activities compositionally in code, inside of NativeActivity, so we are seeing everything there is to see. By the end of Part 2 we ended up with some code for an activity, NativeHandleRequest. And the idea that we want to be using implementation scoping to hide all the implementation details of this activity from the users of the activity, who should just get to see the public surface area. But still we were working with a fairly fuzzy idea of what implementation scope means. Let’s try to drill down into that.]

Public, and implementation. Where do we begin?

When you create a new NativeActivity class it is empty. Your activity has no children, no variables, no arguments, and all-up it has no public surface area that anyone could observe. Also, it has no implementation logic, just a blank Execute() method. Easy.

As soon as you start adding public properties, everything above becomes false.

For example, add a property:

public Activity Body { get; set; }

As soon as we’ve done this, the behavior of base.CacheMetadata() [NativeActivity.CacheMetadata()] kicks in . We now have a public child. There’s some similar default behavior for autodeclaring public Variables and Arguments. All basic stuff.

Next we probably start writing our implementation logic inside of Execute(), which is when we have to start dealing with the rules.

The Rules

  1. Public variables declared on your activity are in public land.
  2. Implementation variables on your activity are in implementation land.
  3. Implementation variables and public variables can never reference each other in expressions, since they can’t see into the other foreign land.
  4. Arguments on your activity bridge the gap. When your argument is initialized with an expression, that expression can refer to variables visible in public land. (But those will probably be variables from elsewhere in the workflow tree - not your own variable.) Consuming the value of an argument, however, is only possible in implementation land .
  5. Inside of Execute(), you are in implementation land.
  6. All of your public child activities live in public land. All of your implementation child activities live in implementation land. Which land they are determines which variables are in scope for those activities.

 

Practically what does this mean?

You cannot access your own public variables directly Execute(), using Variable<>.Get()/Set().
You can access your own implementation variables directly in Execute(). Your activity is the only activity that sees these variables as implementation variables. To everyone else they either appear not to exist, or they appear in the scope.

Public child activities can refer to your public variables via expressions.
Implementation child activities can refer to your implementation variables via expressions.

 

One more interesting case – ActivityDelegates

ActivityDelegate, ActivityAction, and ActivityFunc are super-interesting, because they allow you to mix things up, and solve some problems which could otherwise be... unsolvable?

Notice that with all the above rules there appears to be no way to implement ForEach activity with just a straightforward child activity Body. I mean, how would you do it?

Attempt 1: Body as a public child.

Correct point: Inside Body you can refer to variables throughout the scope of your workflow.
Problem point: Inside Body you cannot get data from anywhere which is accessible to Execute(). The only thing you can see from ForEach is public variables, and they aren’t accessible from Execute(), and so there is no way for Execute() to pass the loop iterator variable to Body.

Attempt 2: Body as an implementation child.

Correct point: We can set an implementation variable in Execute(), and Body can retrieve its value, since it becomes visible in the public scope of Body.
Problem point: We can’t directly refer to any variables anywhere else in the workflow. We could refer to them indirectly by having them passed to Body as InArguments and OutArguments. But this would require us to know exactly what Body is going to do in advance.

The ActivityDelegate loophole solves this problem. We can pass data into the Arguments of any ActivityDelegates we own, inside Execute(), like this (Body is an ActivityAction<T>):

activityContext.ScheduleAction<T>(this.Body, valueToPass, OnDelegateCompleted);

 

In this case Body is declared in CacheMetadata() as a public ActivityDelegate child. And the actual loop body is a public Activity child of Body: Body.Handler. This is how every variable in the public scope of ForEach remains visible to the child.

To summarize the loophole:

ActivityDelegates are the way to cross the implementation land –> public land gap in the opposite direction to regular activity arguments.

 

(Ugly) pictorial representation:

image

That’s all for this post, and hopefully soon we can conclude the series.