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:
- Visual Studio 2015 Update 1 or Visual Studio 2013 Update 3
- xUnit (included)
- EssentialDiagnostics (included)
- 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
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);
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);
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);
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");
}
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.
[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.
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
[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),
},
new DynamicActivityProperty()
{
Name="YY",
Type=typeof(InArgument<int>),
},
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" },
Value = new InArgument<int>(env=> t1.Get(env)),
},
},
};
return s;
},
};
var dic = new Dictionary<string, object>();
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.
[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,
};
var s = new System.Activities.Statements.Sequence()
{
Variables =
{
t1
},
Activities = {
plus,
new System.Activities.Statements.Assign<int>
{
To = new ArgumentReference<int> { ArgumentName="Result" },
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:
- You don't need to define an OutArgument property, since Result is already defined in DynamicActivity<TResult>.
- 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
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)
{
}
}
[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");
}
[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");
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:
This is really puzzling.
So I do further testing.
Example 2
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");
}
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.