通过单元测试学习 Windows Workflow Foundation 4.5:InvokeMethod 和 DynamicActivity
工作流 InvokeMethod 和 DynamicActivity。
背景
我之前开始学习 Windows Workflow Foundation。我倾向于通过系统学习来掌握一项主要技术框架,而不是漫无目的地搜索。然而,我发现大多数写得好的书籍和文章都发布在 2006-2009 年之间,非常过时,尤其是缺失 .NET 4 和 4.5 中的新功能;而近年来为 WF 4.0 和 4.5 出版的几本书写得很差。虽然我通常更喜欢系统、枯燥和抽象的学习,但这次我将准备一些“湿货”材料来学习。
引言
本文重点介绍 InvokeMethod 和 DynamicActivity。
这是本系列的第二篇文章。源代码可在 https://github.com/zijianhuang/WorkflowDemo 找到。
本系列中的其他文章
通过单元测试学习 Windows Workflow Foundation 4.5:CodeActivity
使用代码
源代码可在 https://github.com/zijianhuang/WorkflowDemo 找到。
先决条件
- Visual Studio 2015 Update 1 或 Visual Studio 2013 Update 3
- xUnit(包含)
- EssentialDiagnostics(包含)
- 工作流持久化 SQL 数据库,默认本地数据库为 WF。
本文中的示例来自一个测试类:InvokeMethodTest、DynamicActivityTests、AsyncCodeActivityTests。
InvokeMethod
InvokeMethod 能够方便地通过 Workflow Designer 将现有对象或类型的函数暴露给工作流,无需编程工作,因此您无需编写新的 CodeActivity 派生类。这里的代码示例演示了 InvokeMethod 的运行时行为。
参考文献
示例 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);//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.
}
虽然 InvokeMethod 很好地支持实例方法和静态方法,但调用静态方法的形式自然更简单。因此,如果您有大量静态实用函数,通过 InvokeMethod 将它们引入工作流会很方便且直接。
虽然 InvokeMethod 支持 RunAsynchronously,但该活动并非以“即时并遗忘”的方式运行方法,并且调用者线程仍然会等待新线程完成,即使 InvokeMethod 包含在 Sequence 的其他活动中。
示例 2
这里的第一个场景是期望一个类型的静态函数,第二个场景是期望一个实例函数。请注意,您必须使用委托来引用实例对象。
[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));
}
如果找不到方法或参数验证出现问题,您将收到 InvalidWorkflowException。
示例 3
方法 "ThrowException" 将抛出 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));
}
因此,调用者将让异常直接传递给调用者,即使方法是异步运行的。
DynamicActivity
DynamicActivity 提供了一个对象模型,允许您动态地构建与 WF 设计器和运行时接口的活动,使用 ICustomTypeDescriptor。
参考文献
在运行时使用 DynamicActivity 创建一个 Activity
示例 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),
//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"]);
}
所以基本上,您可以定义 0-n 个 InArgument 属性,0-n 个 OutArgument 属性,并且 Implementation 是一个 Func<Activity> 指针。委托中返回的 Activity 将由 WF 执行。
您可以直接通过每个 DynamicActivityPropery 对象的 Value 属性分配每个 InArgument 属性,或者在调用 DynamicActivity 对象时通过字典进行分配。
通常,执行逻辑已经定义在一个现有的 Activity 对象中,或者通过 Sequence 对现有 Activity 对象进行组合,并通过 Implementation 委托返回。
如果 DynamicActivity 有输出,您需要使用 Assign activity 通过 ArgumentReference 将变量的值分配给 Output 参数。
示例 2
我们可能希望 DynamicActivity 实例返回一个强类型的结果。
[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);
}
这个例子与示例 1 非常相似。区别在于
- 您无需定义 OutArgument 属性,因为 Result 已在 DynamicActivity<TResult> 中定义。
- 在使用 Implementation 中的 Assign activity 时,ArgumentName 必须是 "Result"。
提示
当您在 Workflow Designer 中构建工作流时,您正在构建一个 DynamicActivity,即使 YourProject\obj\Debug 中生成的 YourWorkflow.g.cs 派生自 class Activity。当序列化然后反序列化 Activity 时,恢复的类是 DynamicActivity。
备注:
DynamicActivity 无法序列化。因此,如果您想持久化工作流定义,则不得在工作流中使用 DynamicActivity。
AsyncCodeActivity
抽象类 AsyncCodeActivity 实际上是 InvokeMethod 的基类,而 InvokeMethod 在 WF 设计器中可用。然而,请注意,AsyncCodeActivity 有一些棘手的问题,如下面的示例所示。
示例 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)
{
//not WaitOne
}
}
[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);
}
如果您检查日志文件,您会发现实际上有 3 个活动在两个测试用例中按顺序运行,尽管 AsyncDoSomethingNotWait 类中的 EndExecute() 没有等待。换句话说,WF 运行时总是等待,所谓的异步执行是阻塞的。棘手的是,这与 MSDN 文档 "Creating Asynchronous Activities in WF" 中描述的内容相矛盾。
在书籍 "Windows Workflow Foundation 4 Cookbook" 中,从第 158 页到 161 页的摘要中也明确确认了非阻塞行为。
这真的令人费解。
所以我进行了进一步的测试。
示例 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");
//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
}
如果您检查日志文件,您会看到 "After AsyncHttpGet" 在 AsyncHttpGet 完全完成后才打印出来。
示例 3 和 4
在 演示代码 中,您会发现两个控制台应用程序项目:RunWorkflow.csproj (基于 .NET 4.6.1) 和 RunWF4.csproj (基于 .NET 4)。两者都重新组装了书籍 "Windows Workflow Foundation 4 Cookbook" 中的代码。结果是一致的:MSDN 和 cookbook 中描述的 AsyncCodeActivity 派生类的执行实际上阻塞了调用线程。显然,AsyncCodeActivity 在 .NET 4、.NET 4.5 和 .NET 4.6.1 的当前版本中是损坏的,或者是我遗漏了什么?如果您有其他想法,请发表评论。