Update 01/20/2009: Since I failed to mention this before I wanted to make something clear about these workflow posts. All information in these posts is based on the knowledge I gained through developing a framework based on Windows Workflow 4.0. I am not a member of the team nor is this information in any way endorsed by the team. 

In the previous posts we covered one way you could implement a producer-consumer pattern in workflow. This approach used composition and attempted to hide the complexities of the underlying operations inside of the respective activities through composition. However, one scenario in particular this model does not allow you to implement involves modeling of existing .NET events in a workflow. For instance, if we want to implement a FileSystemWatcher activity we need to provide some way for consumers to hook into the events exposed by the .NET object. The only way to accomplish our goal is to hook up to the .NET events and schedule workflow actions from our event handler – but how would this be implemented? Let’s take a look at some more pieces of the workflow framework that will allow us to accomplish this goal.

Workflow Extensions

Each workflow instance has a collection of objects known as extensions that are able to interface with both the workflow and the host. By default no extensions exist, but they may be added to an instance by the host prior to calling the run method using the WorkflowInstanceExtensionManager. Both WorkflowApplication and WorkflowInvoker provide access to the extension manager by a property named Extensions. There are two distinct ways to attach an extension to a workflow instance using this property:

  • WorkflowInstanceExtensionManager.Add(Object singletonExtension) – Allows you to add a singleton instance of an object to the workflow instance. This is useful if you intend to share a particular extension across instances. It is up to the host to manage the lifetime of the service, although the workflow will remove references to your service.
  • WorkflowInstanceExtensionManager.Add<T>(Func<T> extensionCreationFunction) – Allows you to provide an anonymous function to create your extension on demand. Services created through this mechanism are owned by the workflow instance so the lifetime is managed by the instance as well. If the service object implements IDisposable then the dispose method will be invoked when the instance is unloaded.

There is a third way to add extensions to a workflow instance, and is typically used when an activity requires an extension for correct function. If you have been following through these posts, you may recall our usage of the method NativeActivity.CacheMetadata(NativeActivityMetadata) when implementing the ParallelItemScheduler<T>. In addition to providing a location to declare your arguments and variables, you may also use the method for validation warnings/errors and more importantly providing extension creation functions for required extensions.

Another related scenario for the CacheMetadata method is to inform the runtime that you require an extension that is not provided by you. The following two APIs allow you to specify extension types that you require for an activity to function correctly. If the extension does not exist at runtime a validation error is created informing the user that a required extension does not exist.

If you take a quick look back at the API for adding extensions you’ll notice that an extension is just an Object and is not restricted to any particular base class or type. However, there are some scenarios where the extension may need to interact with the workflow via bookmarks, and in order to do that we need some way to get the WorkflowInstanceProxy (a trimmed down version of WorkflowApplication that provides limited method access from inside a workflow) so we can resume bookmarks. This is where the interface IWorkflowInstanceExtension comes in handy! This interface is defined as follows:

namespace System.Activities.Hosting
{
    public interface IWorkflowInstanceExtension
    {
        IEnumerable<Object> GetAdditionalExtensions();
        void SetInstance(WorkflowInstanceProxy instance);
    }
}

Any time an extension is attached (though either mechanism) that implements the above interface the runtime will invoke both methods. The first method provides an entry point for creating additional extensions. The second method is the more interesting one for our current discussion, as it provides the extension with the owning workflow instance proxy. A typical implementation for this method is to simply store the provided parameter into a member variable for later use by the extension. For illustration purposes, and since it leads us to our next destination, I have pasted an example of how we might design an item scheduling extension for interfacing with our ParallelItemScheduler<T> activity.

public sealed class ItemSchedulerExtension<T> : IWorkflowInstanceExtension
{
    internal ItemSchedulerExtension()
    {
    }

    IEnumerable<Object> IWorkflowInstanceExtension.GetAdditionalExtensions()
    {
        return null;
    }

    void IWorkflowInstanceExtension.SetInstance(WorkflowInstanceProxy instance)
    {
        m_instance = instance;
    }

    public void Schedule(Bookmark bookmark, T item)
    {
        m_instance.BeginResumeBookmark(bookmark, item, s_timeout, new AsyncCallback(OnEndResumeBookmark), null);
    }

    void OnEndResumeBookmark(IAsyncResult result)
    {
        try
        {
            BookmarkResumptionResult resumptionResult = m_instance.EndResumeBookmark(result);
            Debug.Assert(resumptionResult == BookmarkResumptionResult.Success, "Error resuming bookmark. Reason: " + resumptionResult);
        }
        catch (Exception ex)
        {
            Debug.Fail("Exception occurred while resuming bookmark.", ex.ToString());
        }
    }

    private WorkflowInstanceProxy m_instance;
    static readonly TimeSpan s_timeout = TimeSpan.FromMinutes(2);
}

As you can see the implementation of this particular extension is minimal and straightforward. We do not have additional extensions so we can simply return null. Our SetInstance implementation simply stores the workflow instance proxy into our member variable so we can use it in our Schedule implementation. Finally, the schedule method simply resumes a bookmark using the asynchronous pattern for the ResumeBookmark method.

Note: I would like to point out that the synchronous versions of the bookmark methods are intentionally missing from the proxy object, since resuming a bookmark requires the workflow thread to be free. If an activity attempts to resume a bookmark on an asynchronous thread (using this extension) and the workflow thread is busy, the synchronous call blocks until the workflow thread becomes available to schedule the bookmark resumption in the queue. In order to encourage better programming practices and reduce confusion regarding which method to invoke the synchronous methods have been removed.

Conclusion

In this article we discussed workflow extensions, how the lifetime is managed, and how to attach them to a workflow instance. We also discussed how an activity can provide an extension by providing information in the CacheMetadata(*ActivityMetadata) family of methods. If you recall from the introduction I alluded to modeling a .NET event-driven object in a workflow activity. So far we have covered the first piece of the puzzle – next post I plan on revising the producer-consumer model, illustrating a different way to implement the CopyDirectory activity using this new model.