65.9K
CodeProject 正在变化。 阅读更多。
Home

通过单元测试学习 Windows Workflow Foundation 4.5:WorkflowApplication 和 WorkflowServiceHost

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2016 年 3 月 25 日

CPOL

6分钟阅读

viewsIcon

13401

WorkflowApplication 和 WorkflowServiceHost。

背景

最近,我开始学习 Windows Workflow Foundation。我倾向于通过系统性学习来掌握一项重要的技术框架,而不是四处搜索。然而,我发现大多数写得好的书籍和文章都发表于 2006-2009 年,信息已过时,尤其是缺少 .NET 4 中的新功能;而近年来为 WF 4.0 和 4.5 出版的一些书籍质量不高。我通常偏爱系统、枯燥且抽象的学习方式,这次我将制作一些“湿性”材料供学习。

引言

本文将重点介绍 WorkflowApplicationWorkflowServiceHost,并介绍这两个类的基本行为。

这是本系列的第三篇文章。源代码可在 https://github.com/zijianhuang/WorkflowDemo 获取。

本文假定您已阅读过有关 WorkflowApplication 和 WorkflowServiceHost 的文章、教程和示例等,本文更侧重于错误场景。当工作流以“fire and forget”(即时处理)模式或远程运行时,您必须更加关注那些不像桌面程序那样能在屏幕上弹出的错误。

本系列的其他文章如下:

Using the Code

源代码可在https://github.com/zijianhuang/WorkflowDemo找到。

必备组件

  1. Visual Studio 2015 Update 1 或 Visual Studio 2013 Update 3
  2. xUnit(包含)
  3. EssentialDiagnostics(包含)
  4. 工作流持久化 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 应中止工作流。

这会导致在 abort 过程完成后调用 Aborted。未处理的异常用作中止原因。

  取消 指定 WorkflowApplication 应安排根活动的取消并恢复执行。

这会导致在取消过程完成后调用 Completed

  Terminate 指定 WorkflowApplication 应安排根活动的终止并恢复执行。

这会导致在终止过程完成后调用 Completed。未处理的异常用作终止原因。如果未指定 OnUnhandledException 处理程序,则 Terminate 是默认操作。

示例 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 服务或桌面程序等自托管解决方案来说已经足够好。

  1. 您有一个服务合同,并确保 [return ...] 可用于返回数据的操作合同。
  2. 编写实现服务合同的工作流。
  3. 为每个工作流启动一个 WorkflowServiceHost。
  4. 使用相同的服务合同编写客户端代码。

 

兴趣点

您可能已经注意到,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 可以接受任何活动,但在添加了具有与工作流中可用内容匹配的合同的终结点后,服务主机才会变得可通信,并且托管的工作流才能运行。

 

参考文献

 

 

© . All rights reserved.