Providing Context to Custom Workflow Activities

March 9th 2011 Workflow Foundation

When architecting solutions using Workflow Foundation it is typically necessary to provide information to individual activities. But apart from standard input arguments being passed to the workflow or originating from previously executed activities, which can best be modeled using InArgument<> properties, there is often also a need to access some kind of contextual information in the activity. In this post I'm going to discuss three different approaches to providing such context to a custom workflow activity.

The most obvious and simple way of achieving this that comes to mind is of course by declaring another InArgument<> of the required type in your activity. Just make sure that the type you are using is decorated with SerializableAttribute if you plan to use persistence. The solution is far from elegant, though. Every time such an activity will be used in the workflow designer, the additional argument will have to be set to the correct value. Typically you'll have to create an in argument in your workflow and set it to correct value when initializing the workflow. It is doable but also very tedious. I certainly don't suggest doing it this way if the workflows are going to be designed by your end users. It can be a nice shortcut in specific well managed scenarios when you don't need a more sophisticated solution.

// Custom activity class
public class ArgumentContextActivity : CodeActivity
{
    // Input argument with context information
    public InArgument<ContextType> ContextArgument { get; set; }

    protected override void Execute(CodeActivityContext context)
    {
        // Retrieve the context value
        ContextType contextValue = ContextArgument.Get(context);
    }
}

// Test code for running the workflow
[Test]
public void RunArgumentContextWorkflow()
{
    // The context needs to be provided as an input argument
    var inputs = new Dictionary<string, object>() 
    { 
        { "Context", new ContextType() } 
    };
    var results = WorkflowInvoker.Invoke(new ArgumentContextWorkflow(), inputs);
}

ArgumentContextWorkflow definition

A better approach would be using execution properties in combination with a special scope activity. This pattern is already being implemented by CorrelationScope, one of the built-in activities in .NET Framework 4. The basic idea is that the scope activity sets the context values as a named property which can be later retrieved by all its child activities. Ignoring the fact that the property isn't strongly typed and can only be accessed from the property collection based on its string value key, the solution is very suitable in cases which allow the scope activity to generate the context value itself based on the provided input arguments, typically taking care of different independent aspects, such as logging, transactions, security, etc. Again, don't forget about the SerializableAttribute decoration on the context type for persistence to work.

// The scope activity needs a simple designer to add a child activity
[Designer(typeof(PropertyContextScopeDesigner))]
public class PropertyContextScope : NativeActivity
{
    // Hide ChildActivity from the properties window
    [Browsable(false)]
    public Activity ChildActivity { get; set; }

    protected override void Execute(NativeActivityContext context)
    {
        // Create the context and set the property
        context.Properties.Add("Context", new ContextType());
        // Run child activity
        context.ScheduleActivity(ChildActivity);
    }
}

// Custom activity to be used inside the scope
public class PropertyContextActivity : NativeActivity
{
    protected override void Execute(NativeActivityContext context)
    {
        // Retrieve the context value from the property
        ContextType contextValue = (ContextType)context.Properties.Find("Context");
    }
}

// Test code for running the workflow
[Test]
public void RunPropertyContextWorkflow()
{
    // No need to provide an input argument
    var results = WorkflowInvoker.Invoke(new PropertyContextWorkflow());
}

PropertyContextWorkflow definition

The final option for providing context are workflow instance extensions. You can create them by implementing the IWorkflowInstanceExtension interface. Instead of instantiating the context type inside a special activity, you need to provide an instance of it or a delegate for creating a new instance to the workflow runtime, i.e. add one or the other to either the Extensions property of the WorkflowApplication and WorkflowInvoker class or the WorkflowExtensions property of the WorkflowServiceHost class. In both cases you need direct access to the hosting class, meaning that you need to take care of the workflow hosting yourself. If that is not the case your last resort is to provide the delegate for creating the extension inside the overridden CacheMetadata method of your NativeActivity by calling AddDefaultExtensionProvider<> method. No matter how you ensure the extension, you can then access it inside your activity by calling the GetExtension<> method on your ActivityContext class.

// Instance extension class
public class ExtensionContextExtension : IWorkflowInstanceExtension
{
    private ContextType context = new ContextType();

    // Context property for the activity
    public ContextType Context
    {
        get
        {
            return context;
        }
    }

    public IEnumerable<object> GetAdditionalExtensions()
    {
        return null;
    }

    public void SetInstance(WorkflowInstanceProxy instance)
    { }
}

// Custom activity depending on the extension
public class ExtensionContextActivity : CodeActivity
{
    protected override void CacheMetadata(CodeActivityMetadata metadata)
    {
        // Activity should indicate it requires the extension
        metadata.RequireExtension<ExtensionContextExtension>();
    }

    protected override void Execute(CodeActivityContext context)
    {
        // Retrieve the context value from the extension
        ContextType contextValue = 
            context.GetExtension<ExtensionContextExtension>().Context;
    }
}

// Test code for running the workflow
[Test]
public void RunExtensionContextActivity()
{
    // The activity can be run standalone
    WorkflowInvoker invoker = new WorkflowInvoker(new ExtensionContextActivity());

    // Add extension to the runtime before running the workflow
    invoker.Extensions.Add<ExtensionContextExtension>(() => 
        new ExtensionContextExtension());
    var results = invoker.Invoke();
}

I like the last approach best for several reasons:

  • It provides strongly typed access to your context class.
  • Not only is it suitable for providing different aspects as contexts created inside the workflow itself, but it also enables communication out of the workflow runtime directly from the activity because the class can be created outside the runtime and as such can contain references to external objects.
  • Last but not least the context class doesn't need to be serialized as it is created again after the persisted workflow gets rehydrated.

As always make sure you choose the approach which suits your problem best, now that you are aware of all the available options.

Copyright
Creative Commons License