Skip to content
georgiosd edited this page May 8, 2012 · 7 revisions

The Introduction raised the following questions:

  • How do you inject dependencies into activities without having to declare the dependencies as input arguments?
  • How do you build up extensions using DI mechanism?
  • How do you unit test components which host workflow applications or workflow invokers without actually invoking a real workflow?

The extension takes care of injecting dependencies into workflow activities and provides better testable workflow hosts. Let us quickly dive into an example how this extension can be useful. Imagine you have a workflow which does the following:

  1. Observes a given folder for file changes. When a new file arrives in the observed folder the workflow is resumed.
  2. The new file which is indicated by the file changed event is opened and parsed by an IParser implementation
  3. The result is passed to the client which invoked the workflow.

With the extension you can simply do the following:

Define and test drive your parser activity. The dependency to the parser needs to be declared as a public property with getter and setter and either the default injection attribute (InjectAttribute) or your own if you have declared your own injection attribute. In the unit test of the ParseData activity you can simply assign a mock object to the public properties setter.

public sealed class ParseData : CodeActivity
{
    [RequiredArgument]
    public InArgument Path { get; set; }
 
    public OutArgument> ParsedValues { get; set; }
 
    [Inject]
    public IParser Parser { get; set; }
 
    protected override void Execute(CodeActivityContext context)
    {
        var filePath = this.Path.Get(context);
 
        var result = this.Parser.Parse(filePath);
 
        this.ParsedValues.Set(context, result);
    }
}

Define and test drive your folder observation activity. The observation activity needs an extension. Instead of declaring the required extension directly as class you simply define in the CacheMetadata overload that you required an IObserveFolderExtension extension. In the execution method you only get the required IObserveFolderExtension from the context. In your test code for the observation activity you set the FolderWatcher property similar to the ParseData activity to the mocked folder watcher. For the extension testing you need a real WorkflowApplication or WorkflowInvoker and simply register a mocked IObserveFolderExtension on the extension manager.

public sealed class ObserveFolder : NativeActivity
    {
        [RequiredArgument]
        public InArgument Filter { get; set; }
 
        [RequiredArgument]
        public InArgument Folder { get; set; }
 
        public OutArgument Path { get; set; }
 
        [Inject]
        public IFolderWatcher FolderWatcher { get; set; }
 
        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }
 
        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
            // Tell the runtime that we need this extension
            metadata.RequireExtension(typeof(IObserveFolderExtension));
 
            base.CacheMetadata(metadata);
        }
 
        protected override void Execute(NativeActivityContext context)
        {
            var observeExtension = context.GetExtension();
 
            this.FolderWatcher.Folder = this.Folder.Get(context);
            this.FolderWatcher.Filter = this.Filter.Get(context);
 
            observeExtension.AddFileChangedCallback(this.FolderWatcher);
 
            this.FolderWatcher.StartObservation();
 
            context.CreateBookmark(observeExtension.Bookmark, this.OnFileChanged);
        }
 
        private void OnFileChanged(ActivityContext context, Bookmark bookmark, object value)
        {
            this.FolderWatcher.StopObservation();
 
            this.Path.Set(context, (string)value);
        }
    }
 
// Sample test extract with Ron's workflow test helper.
 public class ObserveFolderTest
    {
        private readonly Mock folderWatcher;
 
        private readonly ObserveFolder testee;
 
        private readonly Mock observeFolder;
 
        public ObserveFolderTest()
        {
            this.folderWatcher = new Mock();
            this.observeFolder= new Mock();
 
            this.testee = new ObserveFolder { FolderWatcher = this.folderWatcher.Object, };
        }
 
        private WorkflowApplicationTest ExecuteTestee(string folder, string filter)
        {
            var settings = new { Folder = folder, Filter = filter };
            var workflowTester = WorkflowApplicationTest.Create(this.testee, settings.ToDict());
            workflowTester.TestWorkflowApplication.Extensions.Add(this.observeFolder.Object);
            workflowTester.TestActivity();
 
            return workflowTester;
        }
    }

And finally what about the infrastructure component which creates the workflow application and runs the workflow?

// Extract from hosting component
var workflow = this.kernel.Get();
 
// or use dictionary
var inputs = new { Folder = @"C:\temp", Filter = "*.txt" };
 
workflow.Initialize(new FileInputTransformationWorkflow(), inputs);
 
workflow.OnUnhandledException = this.HandleUnhandledException;
workflow.Completed = this.HandlePresetWorkflowCompleted;
 
// The binding for extensions must be transient to profit from the workflow foundation scoping.
workflow.AddTransientExtension();
workflow.AddSingletonExtension();
 
workflow.Run();
// Extract from possible test
[Fact]
public void Completed_MustRemoveTrackedWorkflow()
{
    Guid workflowIdentification = Guid.NewGuid();
    var workflow = this.GetWorkflow();
    workflow.Setup(w => w.Id).Returns(workflowIdentification);
    workflow.SetupAllProperties();
 
    this.testee.StartPreset(WorkflowClassification);
 
    workflow.Object
        .Completed(new NinjectWorkflowApplicationCompletedEventArgs(
           workflowIdentification,
           ActivityInstanceState.Closed,
           null,
           new Dictionary()));
 
    Assert.Empty(this.testee.RunningWorkflows);
}

That’s so far about the basic usage.

Clone this wiki locally