Dependency Injection with Windows Workflow Foundation 4 an answer

In my last post I 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?

In this follow up post I try to answer these questions.

I wrote an extension for my favorite dependency injection container ninject. 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 new ninject 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<string> Path { get; set; }

        public OutArgument<IDictionary<string, double>> 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<string> Filter { get; set; }

        [RequiredArgument]
        public InArgument<string> Folder { get; set; }

        public OutArgument<string> 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<IObserveFolderExtension>();

            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<IFolderWatcher> folderWatcher;

        private readonly ObserveFolder testee;

        private readonly Mock<IObserveFolderExtension> observeFolder;

        public ObserveFolderTest()
        {
            this.folderWatcher = new Mock<IFolderWatcher>();
            this.observeFolder= new Mock<IObserveFolderExtension>();

            this.testee = new ObserveFolder { FolderWatcher = this.folderWatcher.Object, };
        }

        private WorkflowApplicationTest<FolderWatcherStep> 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<IWorkflowApplication>();

// or use dictionary<string,object>
var inputs = new { Folder = @"C:\temp\", Filter = "*.txt" };

workflow.Initialize(new FileInputTransformationWorkflow(), inputs);

workflow.OnUnhandledException = this.HandleUnhandledException;
workflow.Completed = this.HandlePresetWorkflowCompleted;</pre>

// The binding for extensions must be transient to profit from the workflow foundation scoping.
workflow.AddTransientExtension<IObserveFolderExtension>();
workflow.AddSingletonExtension<ISomeOtherExtension>();

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<string, object>()));

    Assert.Empty(this.testee.RunningWorkflows);
}

That’s so far about the basic usage. See the next post about more advanced stuff! Stay tuned

About the author

Daniel Marbach

10 comments

  • […] Dependency Injection with Windows Workflow Foundation 4 an answer – Daniel Marbach follows up from a previous post about injecting dependencies in Windows Workflow Foundation activities taking a look at using his Ninject extension which provides this functionality. In this post he looks at its use sharing plenty of code samples. […]

  • Do you have the source code I can review for this? I do not see the source for IObserveFolderExtension, or ObserveFolder in the samples. Also, does this work for Castle? Does this work with WCF Workflow Service (xamlx)?

  • Hy Michael,
    The source code is hosted under:

    https://github.com/ninject/ninject.extensions.wf

    The full code of the sample is also in the README

    https://github.com/ninject/ninject.extensions.wf/blob/master/README

    IObserveFolderExtension or ObserveFolder are made up classes which can be dependencies of your workflows.

    What do you mean with Castle? What is your question regarding Castle.Core?

    Designed workflows are a problem. The current implementation needs the InjectAttribute on all properties and can only do property or method injection because the workflow activity is instantiated by the runtime. I haven’t found a solution how to inject into designed workflows.

    Daniel

  • Hi Daniel,

    This is great but has one fundamental problem – it doesn’t work with Microsoft.Activities.UnitTesting – all the methods there expect a WorkflowApplication instance and this is hidden by your wrapper.

    It would be nice if you exposed it or figured another way to use it.

    If you do, please be so kind to drop me a line. Thanks.

  • Hy Georgios,
    Thanks for your comment. Currently I’m in vacation. I think I’ll find some time after the 21th May. In the meantime this might help you:

    You should only be unit testing single activities. Complex workflows should be tested in your developer acceptance tests. If you are unit testing single activities you don’t need a ninject kernel to inject your dependencies in your activity. You can simply set the properties before running the activity. I have used this approach with Microsoft.Activities.UnitTesting without problems. For your developer acceptance tests you should have a kernel and your production code should then only depend upon the interfaces provided in the extension. This should then work like your production environment without changing anything. This allows you to rebind certain dependencies of your workflows prior to execution.

    Hope that helps

    Daniel

  • Hi Daniel,
    Is it possible to upload full example at GitHub for ninject.extensions.wf?

  • Hy Akim
    Is the sample in the readme not enough? Do you want to see any specific scenario?

    Daniel

  • Is this what you have as your Ninject configuration:

    kernel.Bind(IParser ).To().WhenInjectedIntoActivity(typeof(ParseData));

    I’m configuring Ninject like so but my Parser property is always null. Do you have a sample example with Ninject configuration?

  • Hi Anton,
    It is years ago since I last used this library. If I recall correctly it only works when you use the provided NinjectWorkflowApplication or Invoker.

    Regards
    Daniel

By Daniel Marbach

Recent Posts