Click here to Skip to main content
15,886,873 members
Articles / Programming Languages / C#

Learn Windows Workflow Foundation 4.5 through Unit Testing: InvokeMethod and DynamicActivity

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
24 Mar 2016CPOL5 min read 11.5K   5  
Workflow InvokeMethod and DynamicActivity

Background

I had started to learn Windows Workflow Foundation sometime ago. I prefer to learn a major technology framework through systematic study rather then googling around. However, I found that most well written books and articles were published between 2006-2009, so outdated, particularly missing new features in .NET 4 and 4.5; and a few books published in recent years for WF 4.0 and 4.5 were poorly written. While I generally prefer systematic, dry and abstract study, this time I would make up some wet materials for studying.

Introduction

And this article is focused on InvokeMethod and DynamicActivity.

This is the 2nd article in the series. And source code is available at https://github.com/zijianhuang/WorkflowDemo

Other articles in this series:

Learn Windows Workflow Foundation 4.5 through Unit Testing: CodeActivity

Using the code

Source code is available at https://github.com/zijianhuang/WorkflowDemo

Prerequsites:

  1. Visual Studio 2015 Update 1 or Visual Studio 2013 Update 3
  2. xUnit (included)
  3. EssentialDiagnostics (included)
  4. Workflow Persistence SQL database, with default local database WF.

Examples in this article are from a test classe: InvokeMethodTest, DynamicActivityTests, AsyncCodeActivityTests.

InvokeMethod

InvokeMethod is handy for making existing functions of objects or types available to workflow through Workflow Designer without programming effort, so you don't need to write new CodeActivity derived classes. And the code examples here are demonstrating the runtime behaviors of InvokeMethod.

References:

Example 1

C#
public string DoSomething(string s)
{
    System.Threading.Thread.Sleep(200);
    System.Diagnostics.Debug.WriteLine("DoSomething");
    return s;

}

public static int GetSomething()
{
    System.Threading.Thread.Sleep(200);
    System.Diagnostics.Debug.WriteLine("Something");
    return System.Threading.Thread.CurrentThread.ManagedThreadId;
}

[Fact]
public void TestInvokeMethod()
{
    var a = new InvokeMethod<string>()
    {
        MethodName = "DoSomething",
        TargetObject = new InArgument<InvokeMethodTests>(c => this),
        Parameters = { new InArgument<string>("Abcd") },
    };

    var r = WorkflowInvoker.Invoke(a);//method GetSomething() run in the same thread
    System.Diagnostics.Debug.WriteLine("Something invoke");
    Assert.Equal("Abcd", r);
}

[Fact]
public void TestInvokeStaticMethod()
{
    var a = new InvokeMethod<int>()
    {
        MethodName = "GetSomething",
        TargetType = this.GetType(),
    };

    var r = WorkflowInvoker.Invoke(a);//method GetSomething() run in the same thread
    System.Diagnostics.Debug.WriteLine("Something invoke");
    Assert.Equal(System.Threading.Thread.CurrentThread.ManagedThreadId, r);
}

[Fact]
public void TestInvokeStaticMethodAsync()
{
    var a = new InvokeMethod<int>()
    {
        MethodName = "GetSomething",
        TargetType = this.GetType(),
        RunAsynchronously = true,
    };

    var r = WorkflowInvoker.Invoke(a);//run in a new thread, however, wait for it finished.
    System.Diagnostics.Debug.WriteLine("Something invoke");
    Assert.NotEqual(System.Threading.Thread.CurrentThread.ManagedThreadId, r);

}

[Fact]
public void TestInvokeStaticMethodAsyncInSequence()
{
    var t1 = new Variable<int>("t1");
    var a = new InvokeMethod<int>()
    {
        MethodName = "GetSomething",
        TargetType = this.GetType(),
        RunAsynchronously = true,
        Result = t1,
    };

    var s = new System.Activities.Statements.Sequence()
    {
        Variables = { t1 },
        Activities = {
            new Plus() {X=2, Y=3 },
            a,
            new Multiply() {X=3, Y=7 },
        },

    };

    var r = WorkflowInvoker.Invoke(s);
    System.Diagnostics.Debug.WriteLine("Something invoke");
    //So all run in sequences. The async activity is not being executed in fire and forget style, but probably just good not freezing the UI thread if UI is involved.

}

While InvokeMethod well supports both instance methods and static methods, the form of calling static methods is simpler naturally. So if you have a large set of static utility functions, it is convenient and straightforward to introduce them into workflow through InvokeMethod.

While InvokeMethod supports RunAsynchronously, however, the activity is not running the method in fire and forget style, and the caller thread still wait for the new thread to finish, even InvokeMethod is among other activities in Sequence.

Example 2

The first case here is expecting a static function of a type, and the second is expecting an instance function. Noted that you have to use a delegate to reference to the instance object.

C#
[Fact]
public void TestInvokeStaticMethodMissingThrows()
{
    var a = new InvokeMethod<int>()
    {
        MethodName = "GetSomethingMissing",
        TargetType = this.GetType(),
    };

    Assert.Throws<InvalidWorkflowException>(() => WorkflowInvoker.Invoke(a));
}

[Fact]
public void TestInvokeMethodMissingparametersThrows()
{
    var a = new InvokeMethod<string>()
    {
        MethodName = "DoSomething",
        TargetObject = new InArgument<InvokeMethodTests>(c => this),
    };

    Assert.Throws<InvalidWorkflowException>(() => WorkflowInvoker.Invoke(a));
}

If the method is not found or the validation of parameters is having problems, you will get InvalidWorkflowException.

Example 3

The method "ThrowException" will throw InvalidProgramException.

C#
public static void ThrowException()
{
    throw new InvalidProgramException("Just a funky test");
}

[Fact]
public void TestInvokeStaticMethodThatThrows()
{
    var a = new System.Activities.Statements.InvokeMethod()
    {
        MethodName = "ThrowException",
        TargetType = this.GetType(),
    };

    Assert.Throws<InvalidProgramException>(()=> WorkflowInvoker.Invoke(a));
}

[Fact]
public void TestInvokeStaticMethodAsyncThatThrows()
{
    var a = new System.Activities.Statements.InvokeMethod()
    {
        MethodName = "ThrowException",
        TargetType = this.GetType(),
        RunAsynchronously=true,
    };

    Assert.Throws<InvalidProgramException>(() => WorkflowInvoker.Invoke(a));
}

 

So the invoker will just let the exception pass through to the caller, even if the method is running asynchronously.

DynamicActivity

DynamicActivity provides an object model that allows you to construct activities dynamically that interface with the WF designer and runtime using ICustomTypeDescriptor.

References:

Create an Activity at Runtime with DynamicActivity

Example 1

C#
[Fact]
public void TestDynamicActivity()
{
    var x = 100;
    var y = 200;
    var a = new DynamicActivity
    {
        DisplayName = "Dynamic Plus",
        Properties =
        {
            new DynamicActivityProperty()
            {
                Name="XX",
                Type= typeof(InArgument<int>),
                Value=new InArgument<int>(x),
                //You can't do Value=x, otherwise, System.InvalidCastException : Unable to cast object of type 'System.Int32' to type 'System.Activities.Argument'

            },
            new DynamicActivityProperty()
            {
                Name="YY",
                Type=typeof(InArgument<int>),
                //Value=y,
            },
            new DynamicActivityProperty()
            {
                Name="ZZ",
                Type=typeof(OutArgument<int>),
            }

        },

        Implementation = () =>
        {
            Variable<int> t1 = new Variable<int>("t1");

            var plus = new Plus()
            {
                X = new ArgumentValue<int>() { ArgumentName = "XX" },
                Y = new ArgumentValue<int>() { ArgumentName = "YY" },
                Z = t1,
            };

            var s = new System.Activities.Statements.Sequence()
            {
                Variables =
                {
                    t1
                },
                Activities = {
                    plus,

                    new System.Activities.Statements.Assign<int>
                    {
                        To = new ArgumentReference<int> { ArgumentName = "ZZ" },//So the Value will be assigned to property ZZ. Noted that ArgumentReference<> is a CodeActivity<>
                        Value = new InArgument<int>(env=> t1.Get(env)),  //So the Value  will be wired from t1 in context.
                    },

                },
            };
            return s;
        },

    };

    var dic = new Dictionary<string, object>();
   // dic.Add("XX", x);
    dic.Add("YY", y);

    var r = WorkflowInvoker.Invoke(a, dic);
    Assert.Equal(300, (int)r["ZZ"]);
}

So basically you may define 0-n properties of InArgument, and 0-n properties of OutArgument, and the Implementation is a Func<Activity> pointer. And the Activity returned in the delegate will be executed by WF.

You can either assign each InArgument property through the Value property of each DynamicActivityPropery object, or assign through a dictionary when invoking the DynamicActivity object.

Generally the execution logic is already defined in an existing Activity object, or a composition of existing Activity objects through Sequence, returned by the Implementation delegate.

If the DynamicActivity has an output, you need to have the Assign activity to assign the value of a variable to the Output argument through ArgumentReference.

Example 2

We may prefer that a DynamicActivity instance returns a strongly typed result.

C#
[Fact]
public void TestDynamicActivityGeneric()
{
    var x = 100;
    var y = 200;
    var a = new DynamicActivity<int>
    {
        DisplayName = "Dynamic Plus",
        Properties =
        {
            new DynamicActivityProperty()
            {
                Name="XX",
                Type= typeof(InArgument<int>),

            },
            new DynamicActivityProperty()
            {
                Name="YY",
                Type=typeof(InArgument<int>),
            },

        },

        Implementation = () =>
        {
            var t1 = new Variable<int>("t1");

            var plus = new Plus()
            {
                X = new ArgumentValue<int>() { ArgumentName = "XX" },
                Y = new ArgumentValue<int>() { ArgumentName = "YY" },
                Z = t1,  //So result will be assigned to t1
            };
            var s = new System.Activities.Statements.Sequence()
            {
                Variables =
                {
                    t1
                },
                Activities = {
                    plus,
                    new System.Activities.Statements.Assign<int>
                    {
                        To = new ArgumentReference<int> { ArgumentName="Result" },//I just had a good guess about how Result get assigned.
                        Value = new InArgument<int>(env=> t1.Get(env)),
                    },

                },
            };
            return s;
        },

    };

    var dic = new Dictionary<string, object>();
    dic.Add("XX", x);
    dic.Add("YY", y);

    var r = WorkflowInvoker.Invoke(a, dic);
    Assert.Equal(300, r);
}

This example is very similar to example 1. The differences:

  1. You don't need to define an OutArgument property, since Result is already defined in DynamicActivity<TResult>.
  2. When using activity Assign in Implementation, the ArgumentName must be "Result".

Hints:

When you are constructing a workflow in Workflow Designer, you are constructing a DynamicActivity, even though YourWorkflow.g.cs generated in YourProject\obj\Debug is derived from class Activity. And when serializing and then deserializing an Activity, the restored class is DynamicActivity.

Remarks:

DynamicActivity cannot be serialized. Thus, if you want to persist a workflow definition, you must not use DynamicActivity inside the workflow.

AsyncCodeActivity

Abstract class AsyncCodeActivity is actually the base class of InvokeMethod, while InvokeMethod is available in WF designer. However, please be aware, there are some tricky things with AsyncCodeActivity as shown in the examples below.

Example 1

C#
public class AsyncDoSomethingAndWait : AsyncCodeActivity
{

    int DoSomething()
    {
        System.Threading.Thread.Sleep(1100);
        System.Diagnostics.Trace.TraceInformation("Do AsyncDoSomethingAndWait");
        return System.Threading.Thread.CurrentThread.ManagedThreadId;
    }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        Func<int> d = () => DoSomething();
        return d.BeginInvoke(callback, state);
    }

    protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        result.AsyncWaitHandle.WaitOne();
    }
}

public class AsyncDoSomethingNotWait : AsyncCodeActivity
{

    int DoSomething()
    {
        System.Threading.Thread.Sleep(3100);
        System.Diagnostics.Trace.TraceInformation("Do AsyncDoSomethingNotWait");
        return System.Threading.Thread.CurrentThread.ManagedThreadId;
    }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        Func<int> d = () => DoSomething();
        return d.BeginInvoke(callback, state);
    }

    protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        //not WaitOne
    }
}

 

C#
[Fact]
public void TestAsyncDoSomethingInSequence()
{
    System.Diagnostics.Debug.WriteLine("TestAsyncDoSomethingInSequence");
    var a = new AsyncDoSomethingAndWait();
    var s = new System.Activities.Statements.Sequence()
    {
        Activities = {
            new Plus() {X=2, Y=3 },
            a,
            new Multiply() {X=3, Y=7 },
        },
    };

    var r = WorkflowInvoker.Invoke(s);
    System.Diagnostics.Debug.WriteLine("After AsyncDoSomething in Sequence invoke");
    //check the log file, the invoker will just run 3 activities one by one, and waiting for a to finish, though the key function of a is running in a new thread
}

[Fact]
public void TestAsyncDoSomethingNotWaitInSequence()
{
    System.Diagnostics.Debug.WriteLine("TestAsyncDoSomethingNotWaitInSequence");
    var a = new AsyncDoSomethingNotWait();
    var s = new System.Activities.Statements.Sequence()
    {
        Activities = {
            new Plus() {X=2, Y=3 },
            a,
            new Multiply() {X=3, Y=7 },
        },
    };

    var r = WorkflowInvoker.Invoke(s);
    System.Diagnostics.Debug.WriteLine("After AsyncDoSomethingNotWait in Sequence invoke");
    //check the log file, the invoker will just run 3 activities one by one, and waiting for a to finish, though the key function of a is running in a new thread
    System.Threading.Thread.Sleep(1100);
}

 

If you check the log file, you will see that 3 activities are actually running in sequence in both test cases, regardless that EndExecute() in class AsyncDoSomethingNotWait does not wait. In other words, the WF runtime always wait, and the so called asynchronous execution is blocking. The tricky thing is, this is contradicting against what described in MSDN documentation "Creating Asynchronous Activities in WF".

And in book "Windows Workflow Foundation 4 Cookbook", the non-blocking behavior was apparently confirmed in the digest from page 158 to 161:

Image 1

Image 2

Image 3

Image 4

 

This is really puzzling.

So I do further testing.

Example 2

C#
public class AsyncHttpGet : AsyncCodeActivity<string>
{
    public InArgument<string> Uri { get; set; }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        var uri = Uri.Get(context);
        WebRequest request = HttpWebRequest.Create(uri);
        context.UserState = request;
        return request.BeginGetResponse(callback, state);
    }

    protected override string EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        WebRequest request = context.UserState as WebRequest;
        using (WebResponse response = request.EndGetResponse(result))
        {
            using (StreamReader reader = new StreamReader(response.GetResponseStream()))
            {
                var s = reader.ReadToEnd();
                Console.WriteLine(s);
                System.Diagnostics.Trace.TraceInformation(s);
                return s;
            }
        }
    }
}

    [Fact]
    public void TestAsyncHttpGetInSequence()
    {
        System.Diagnostics.Debug.WriteLine("TestAsyncHttpGetInSequence2");
        var a = new AsyncHttpGet() { Uri = "http://fonlow.com" };
        var s = new System.Activities.Statements.Sequence()
        {
            Activities = {
                new WriteLine() {Text="Before AsyncHttpGet", TextWriter=new InArgument<System.IO.TextWriter>((c)=> new Fonlow.Utilities.TraceWriter()) },
                a,
                new WriteLine() {Text="After AsyncHttpGet", TextWriter=new InArgument<System.IO.TextWriter>((c)=> new Fonlow.Utilities.TraceWriter()) },
            },
        };

        var r = WorkflowInvoker.Invoke(s);
        System.Diagnostics.Debug.WriteLine("After AsyncHttpGet in Sequence invoke");
        //check the log file, the invoker will just run 3 activities one by one, and waiting for a to finish, though the key function of a is running in a new thread
    }

 

If you check the log file, you will see that "After AsyncHttpGet" is printed out after AsyncHttpGet is completely finished.

Example 3 and 4

In the demo code, you will find 2 console app projects: RunWorkflow.csproj on .NET 4.6.1 and RunWF4.csproj on .NET 4. Both reassemble the codes in book "Windows Workflow Foundation 4 Cookbook". And the results are consistent: the executions of AsyncCodeActivity derived classes described in MSDN and the cookbook are actually blocking the caller thread. Apparently AsyncCodeActivity is broken in current releases of .NET 4, .NET 4.5 and .NET 4.6.1, or did I miss something? If you have other idea, please leave a comment.

 

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
-- There are no messages in this forum --