通过单元测试学习 Windows Workflow Foundation 4.5:WorkflowApplication 和 WorkflowServiceHost
WorkflowApplication 和 WorkflowServiceHost。
背景
最近,我开始学习 Windows Workflow Foundation。我倾向于通过系统性学习来掌握一项重要的技术框架,而不是四处搜索。然而,我发现大多数写得好的书籍和文章都发表于 2006-2009 年,信息已过时,尤其是缺少 .NET 4 中的新功能;而近年来为 WF 4.0 和 4.5 出版的一些书籍质量不高。我通常偏爱系统、枯燥且抽象的学习方式,这次我将制作一些“湿性”材料供学习。
引言
本文将重点介绍 WorkflowApplication
和 WorkflowServiceHost
,并介绍这两个类的基本行为。
这是本系列的第三篇文章。源代码可在 https://github.com/zijianhuang/WorkflowDemo 获取。
本文假定您已阅读过有关 WorkflowApplication 和 WorkflowServiceHost 的文章、教程和示例等,本文更侧重于错误场景。当工作流以“fire and forget”(即时处理)模式或远程运行时,您必须更加关注那些不像桌面程序那样能在屏幕上弹出的错误。
本系列的其他文章如下:
- 通过单元测试学习 Windows Workflow Foundation 4.5:CodeActivity
- 通过单元测试学习 Windows Workflow Foundation 4.5:InvokeMethod 和 DynamicActivity
Using the Code
源代码可在https://github.com/zijianhuang/WorkflowDemo找到。
必备组件
- Visual Studio 2015 Update 1 或 Visual Studio 2013 Update 3
- xUnit(包含)
- EssentialDiagnostics(包含)
- 工作流持久化 SQL 数据库,默认本地数据库为 WF。
本文中的示例来自测试类:WorkflowApplicationTests, WorkflowServiceHostTests。
WorkflowApplication
下面的几个测试用例展示了 WorkflowApplication
在错误情况下的行为,这样您就可以更好地了解如何在应用程序中处理类似情况。
示例 1
[Fact]
public void TestWorkflowApplication()
{
AutoResetEvent syncEvent = new AutoResetEvent(false);
var a = new System.Activities.Statements.Sequence()
{
Activities =
{
new System.Activities.Statements.Delay()
{
Duration= TimeSpan.FromSeconds(2),
},
new Multiply()
{
X = 3,
Y = 7,
}
},
};
var app = new WorkflowApplication(a);
int mainThreadId = Thread.CurrentThread.ManagedThreadId;
int workFlowThreadId = -1;
app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
workFlowThreadId = Thread.CurrentThread.ManagedThreadId;
syncEvent.Set();
};
var dt = DateTime.Now;
app.Run();
Assert.True((DateTime.Now - dt).TotalSeconds < 1, "app.Run() should not be blocking");
syncEvent.WaitOne();
Assert.NotEqual(mainThreadId, workFlowThreadId);
}
这个测试用例表明,WorkflowApplication
实例在新线程中运行 Sequence
活动,并且 app.Run
不是阻塞的。
备注
根据 MSDN 关于 WorkflowApplication.Run() 的说明
调用此方法以启动新创建的工作流实例的执行。
如果在 30 秒内运行操作未完成,则会抛出 TimeoutException。
这实际上意味着,如果 .NET 运行时分配资源运行 WorkflowApplication 耗时超过 30 秒,就会抛出 TimeoutException。显然,进程中运行的第一个 WorkflowApplication 可能需要超过 1 秒才能启动执行,而后续实例可能无需时间。如果资源紧张,.NET 运行时执行 Run() 的时间可能会更长。
示例 2
public class ThrowSomething : CodeActivity
{
protected override void Execute(CodeActivityContext context)
{
throw new NotImplementedException("nothing");
}
}
[Fact]
public void TestWorkflowApplicationCatchException()
{
AutoResetEvent syncEvent = new AutoResetEvent(false);
var a = new ThrowSomething();
var app = new WorkflowApplication(a);
bool exceptionHandled = false;
bool aborted = false;
int mainThreadId = Thread.CurrentThread.ManagedThreadId;
int workFlowThreadId = -1;
app.OnUnhandledException = (e) =>
{
Assert.IsType<NotImplementedException>(e.UnhandledException);
exceptionHandled = true;
workFlowThreadId = Thread.CurrentThread.ManagedThreadId;
return UnhandledExceptionAction.Abort;
};
app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
Assert.True(false, "Never completed");
syncEvent.Set();
};
app.Aborted = (eventArgs) =>
{
aborted = true;
syncEvent.Set();
};
app.Run();
syncEvent.WaitOne();
Assert.True(exceptionHandled);
Assert.True(aborted);
Assert.NotEqual(mainThreadId, workFlowThreadId);
}
WorkflowApplication
通过 OnUnhandledException
内置了处理未捕获异常的机制,因此未捕获的异常不会传播到调用者线程。
备注
我在遗留代码中发现,一些程序员会通过 WorkflowApplication 作用域外部的变量来保存异常对象,然后在阻塞调用 syncEvent.WaitOne() 后重新抛出异常。这种做法实际上破坏了 WorkflowApplication 的设计目的。
示例 3
[Fact]
public void TestWorkflowApplicationWithoutOnUnhandledException()
{
AutoResetEvent syncEvent = new AutoResetEvent(false);
var a = new ThrowSomething();
var app = new WorkflowApplication(a);
bool aborted = false;
bool completed = false;
app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
completed = true;
syncEvent.Set();
};
app.Aborted = (eventArgs) =>
{
aborted = true;
syncEvent.Set();
};
app.Run();
syncEvent.WaitOne();
Assert.True(completed);
Assert.False(aborted);
}
在此测试用例中,正在执行的工作流抛出了异常,但是 Completed
委托被调用,而 Aborted
没有被调用。这是设计好的行为,如 MSDN 所述。
成员名称 | 描述 | |
Abort | 指定 WorkflowApplication 应中止工作流。 这会导致在 |
|
取消 | 指定 WorkflowApplication 应安排根活动的取消并恢复执行。 这会导致在取消过程完成后调用 Completed。 |
|
Terminate | 指定 WorkflowApplication 应安排根活动的终止并恢复执行。 这会导致在终止过程完成后调用 Completed。未处理的异常用作终止原因。如果未指定 OnUnhandledException 处理程序,则 |
示例 4
[Fact]
public void TestWorkflowApplicationNotCatchExceptionWhenValidatingArguments()
{
var a = new Multiply()
{
Y = 2,
};
var app = new WorkflowApplication(a);
//None of the handlers should be running
app.OnUnhandledException = (e) =>
{
Assert.True(false);
return UnhandledExceptionAction.Abort;
};
app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
Assert.True(false);
};
app.Aborted = (eventArgs) =>
{
Assert.True(false);
};
app.Unloaded = (eventArgs) =>
{
Assert.True(false);
};
Assert.Throws<ArgumentException>(() => app.Run());//exception occurs during
//validation and in the same thread of the caller, before any activity runs.
}
在这种情况下,工作流根本没有在 WorkflowApplication
中执行。要执行的活动缺少参数 X
。参数的验证发生在调用者线程中,因此 ArgumentException
会在调用者线程中抛出。
WorkflowServiceHost
如果您进行了大量的 WCF 开发,您应该很容易理解 WorkflowServiceHost
。然而,在 WF 中,有两个名为 WorkflowServiceHost
的类。
WF3.5 中的 WorkflowServiceHost 类
注意:此 API 已过时。
继承层次结构
System. Object
System.ServiceModel.Channels. CommunicationObject
System.ServiceModel. ServiceHostBase
System.ServiceModel. WorkflowServiceHost
Namespace: System.ServiceModel
Assembly:System.WorkflowServices (in System.WorkflowServices.dll)
WF4 和 WF4.5 中的 WorkflowServiceHost 类
继承层次结构
System. Object
System.ServiceModel.Channels. CommunicationObject
System.ServiceModel. ServiceHostBase
System.ServiceModel.Activities. WorkflowServiceHost
Namespace: System.ServiceModel.Activities
Assembly:System.ServiceModel.Activities (in System.ServiceModel.Activities.dll)
因此,请确保您引用了正确的程序集和命名空间,以获取 WF4.5 中正确的 WorkflowServiceHost 类。
示例 1:简单的单向操作以启动工作流
[ServiceContract]
public interface ICountingWorkflow
{
[OperationContract(IsOneWay = true)]
void start();
}
const string connectionString = "Server =localhost; Initial Catalog = WF; Integrated Security = SSPI";
const string hostBaseAddress = "net.tcp:///CountingService";
[Fact]
public void TestOpenHost()
{
// Create service host.
WorkflowServiceHost host = new WorkflowServiceHost
(new Microsoft.Samples.BuiltInConfiguration.CountingWorkflow(), new Uri(hostBaseAddress));
// Add service endpoint.
host.AddServiceEndpoint("ICountingWorkflow", new NetTcpBinding(), "");
host.Open(TimeSpan.FromSeconds(2));
Assert.Equal(CommunicationState.Opened, host.State);
// Create a client that sends a message to create an instance of the workflow.
ICountingWorkflow client = ChannelFactory<ICountingWorkflow>.CreateChannel
(new NetTcpBinding(), new EndpointAddress(hostBaseAddress));
client.start();
host.Close();
}
备注
Microsoft 的 Workflow Samples 使用 HttpBinding
。然而,HttpBinding
侦听需要管理员权限,而我更喜欢以普通用户身份运行 Visual Studio。因此,在单元测试期间,我通常使用 NetTcpBinding
作为服务主机。首次运行将创建和打开服务主机的测试套件时,Windows 防火墙会提示允许打开一个端口供测试套件使用。
在点击“允许访问”后,您将获得 2 条新规则,测试套件将继续运行。
这种配置在您的开发 PC 上只需进行一次。
示例 2:合同与工作流不匹配
[Fact]
public void TestOpenHostWithoutContractImpThrows()
{
// Create service host.
using (WorkflowServiceHost host = new WorkflowServiceHost(new Plus(), new Uri(hostBaseAddress)))
{
Assert.Throws<InvalidOperationException>(()
=> host.AddServiceEndpoint("ICountingWorkflow", new NetTcpBinding(), ""));
Assert.Equal(CommunicationState.Created, host.State);
}
}
在创建 WorkflowServiceHost
实例期间,如果待添加的终结点的合同与托管的工作流不匹配,将会发生 InvalidOperationException
。但是,服务主机仍然被创建。显然,您需要设计一种方法来处理无效或已失效的服务主机对象。
示例 3:OperationContract 返回工作流结果
[ServiceContract(Namespace ="http://fonlow.com/workflowdemo/")]
public interface ICalculation
{
[OperationContract]
[return: MessageParameter(Name = "Result")]
long MultiplyXY(int parameter1, int parameter2);
}
[Fact]
public void TestMultiplyXY()
{
// Create service host.
using (WorkflowServiceHost host = new WorkflowServiceHost(new Fonlow.Activities.MultiplyWorkflow2(), new Uri(hostBaseAddress)))
{
Debug.WriteLine("host created.");
// Add service endpoint.
host.AddServiceEndpoint("ICalculation", new NetTcpBinding(), "");
host.Open();
Debug.WriteLine("host opened");
Assert.Equal(CommunicationState.Opened, host.State);
// Create a client that sends a message to create an instance of the workflow.
var client = ChannelFactory<ICalculation>.CreateChannel(new NetTcpBinding(), new EndpointAddress(hostBaseAddress));
var r = client.MultiplyXY(3, 7);
Assert.Equal(21, r);
}
}
备注
客户端使用的操作合同必须与工作流中 Receive 和 SendReply 定义的参数和响应匹配,否则服务端的运行时数据绑定将失败。通常,您必须确保 [return: MessageParameter...] 与 SendReply 内容的响应中的参数名称匹配。因此,在实际项目中,最好生成客户端 API 代码以确保精确匹配。
通用步骤
一个包含 Receive 的工作流可以通过将工作流(活动/XAML)托管在 WorkflowServiceHost 中来成为一个 WCF 服务,这种结构对于 Windows 服务或桌面程序等自托管解决方案来说已经足够好。
- 您有一个服务合同,并确保 [return ...] 可用于返回数据的操作合同。
- 编写实现服务合同的工作流。
- 为每个工作流启动一个 WorkflowServiceHost。
- 使用相同的服务合同编写客户端代码。
兴趣点
您可能已经注意到,WorkflowApplication 也有 BeginRun() 和 EndRun()。我发现它们在实际应用中的用途很小。如果您调用这对方法而不是 Run(),Completed 和 OnUnhandledException 等回调将永远不会被调用,从而失去了访问 WF 运行时更多特性的优势。您可以查看测试用例:BasicTests.WorkflowApplicationTests.TestWorkflowApplicationBeginRun() 和 TestWorkflowApplicationBeginRunWithException()。如果我不在乎 WF 运行时更多特性,只想以 Begin/End 方式运行工作流,我可能会简单地使用委托的 BeginInvoke()。如果您对 BeingRun() 和 EndRun() 有不同的看法,请留下评论。
虽然 WorkflowApplication 和 WorkflowServiceHost 都可以访问 WF 运行时,但只有 WorkflowServiceHost 可以侦听远程请求。例如,如果工作流包含 Receive,您必须通过 WorrkflowServiceHost 运行它,而运行这种包含 Receive 的工作流在 WorkflowApplication 中是没有用的,因为它无法侦听并将消息传递给 Receive。
如果某个活动/工作流不包含 Receive,那么将其托管在 WorkflowServiceHost 中是没有意义的,因为 ServiceHost 只有在至少有一个由合同、绑定和地址组成的终结点时才有用。虽然 WorkflowServiceHost 可以接受任何活动,但在添加了具有与工作流中可用内容匹配的合同的终结点后,服务主机才会变得可通信,并且托管的工作流才能运行。
参考文献