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

通过单元测试学习 Windows Workflow Foundation 4.5:CodeActivity

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2016年3月20日

CPOL

3分钟阅读

viewsIcon

14211

工作流 CodeActivity。

背景

我之前开始学习 Windows Workflow Foundation。我更喜欢通过系统学习来学习一项主要的技术框架,而不是四处 Google。然而,我发现大多数写得好的书籍和文章都发表在 2006-2009 年之间,已经过时了,特别是缺少 .NET 4 和 4.5 中的新功能;而且近年来出版的 WF 4.0 和 4.5 的几本书写得很差。虽然我通常更喜欢系统、枯燥和抽象的学习,但这次我将制作一些湿材料用于学习。

引言

虽然 WF 提供了丰富的功能,但对于 .NET 开发人员来说,.NET 开发人员可能主要使用 WF 来编写 CodeActivity 并在 WF 应用程序中使用它们。

我将跳过您在 MSDN 和当前可用的教程中读到的内容。并且本文重点是提供代码示例,以加强您对设计活动和使用它们的记忆。

这是本系列的第一篇文章。

本系列中的其他文章

通过单元测试学习 Windows Workflow Foundation 4.5:InvokeMethod 和 DynamicActivity

使用代码

源代码可在 https://github.com/zijianhuang/WorkflowDemo 获取

先决条件

  1. Visual Studio 2015 Update 1 或 Visual Studio 2013 Update 3
  2. xUnit(包含)
  3. EssentialDiagnostics(包含)
  4. 工作流持久化 SQL 数据库,默认本地数据库为 WF。

本文中的示例来自一个测试类:Basic。

CodeActivity

经典输出方式

    public class Plus : CodeActivity
    {
        protected override void Execute(CodeActivityContext context)
        {
            Z.Set(context, X.Get(context) + Y.Get(context));
        }

        public InArgument<int> X { get; set; }

        public InArgument<int> Y { get; set; }

        public OutArgument<int> Z { get; set; }//client code accesses Z through dictionary
    }



        [Fact]
        public void TestPlusWithDicOutput()
        {
            var a = new Plus()
            {
                X = 1,
                Y = new InArgument<int>(2),
            };

            var dic = WorkflowInvoker.Invoke(a);
            Assert.Equal(3, (int)dic["Z"]);
        }

        [Fact]
        public void TestPlusWithDicInput()
        {
            var a = new Plus();

            var inputs = new Dictionary<string, object>()
            {
                {"X", 1 },
                {"Y", 2 }
            };

            var dic = WorkflowInvoker.Invoke(a, inputs);
            Assert.Equal(3, (int)dic["Z"]);
        }

        [Fact]
        public void TestPlusWithDefaultValue()
        {
            var a = new Plus()
            {
                Y = 2, //X not assigned, thus will have the default value 0 when being invoked.
            };

            Assert.Null(a.X);
            var dic = WorkflowInvoker.Invoke(a);
            Assert.Equal(2, (int)dic["Z"]);
            Assert.NotNull(a.X);
        }

 

并且您可以拥有多个 OutArgument 属性,每个属性都可以通过 WorkflowInvoker.Invoke() 返回的字典进行访问。

备注

您可能注意到 InArgument<T> 属性被赋予了 T 的值/表达式,并且 .NET 编译器和运行时将围绕每个赋值的值/表达式创建一个 InArgument 包装器。

 

在 .NET 4 中进行强类型输出

显然,通过对象字典访问输出对于习惯于强类型数据的开发人员来说并不有吸引力。在 .NET 4 中,WF 通过 CodeActivity<TResult> 类支持强类型输出。

    public class Multiply : CodeActivity<long>
    {
        protected override long Execute(CodeActivityContext context)
        {
            var r= X.Get(context) * Y.Get(context);
            Z.Set(context, r);
            return r;
        }

        [RequiredArgument]
        public InArgument<int> X { get; set; }

        [RequiredArgument]
        public InArgument<int> Y { get; set; }

        /// <summary>
        /// This is compiled however in production codes, OutArgument should not be defined.
        /// </summary>
        public OutArgument<long> Z { get; set; }

    }

        [Fact]
        public void TestMultiplyWithTypedOutput()
        {
            var a = new Multiply()
            {
                X = 3,
                Y = 2,
            };

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

        [Fact]
        public void TestMultiplyMissingRequiredThrows()
        {
            var a = new Multiply()
            {
                //           X = 3,
                Y = 2,
            };

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

尽管在示例中有一个 OutArgument 属性,但它没有用,因为代码中没有接口可以访问它,尽管 Workflow Designer 可以访问它。它是多余的。因此,在生产代码中,Result 就足够了。

必需的参数

InArguments 由 RequiredArgumentAttribute 装饰,因此它们必须被赋值,否则 WF 运行时将在验证输入时抛出 ArgumentException。

重载组

有时您希望拥有两组 InAgruments,但只能分配其中一组,因此您可能拥有重载组。

    public class QuerySql : CodeActivity
    {

        [RequiredArgument]
        [OverloadGroup("G1")]
        public InArgument<string> ConnectionString { get; set; }

        [RequiredArgument]
        [OverloadGroup("G2")]
        public InArgument<string> Host { get; set; }

        [OverloadGroup("G2")]
        public InArgument<string> Database { get; set; }

        [OverloadGroup("G2")]
        public InArgument<string> User { get; set; }

        [OverloadGroup("G2")]
        public InArgument<string> Password { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            //do nothing here
        }
    }

        [Fact]
        public void TestOverloadGroup()
        {
            var a = new QuerySql()
            {
                ConnectionString="cccc",
            };

            var r= WorkflowInvoker.Invoke(a);
            
        }
        [Fact]
        public void TestOverloadGroupWithBothGroupsAssignedThrows()
        {
            var a = new QuerySql()
            {
                ConnectionString = "cccc",
                Host="localhost"
            };

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

 

内置的乘法活动

上面的 Multiply 类用于演示目的,事实上 .NET Framework 4 已经提供了 Multiply< TLeft, TRight, TResult>.

        [Fact]
        public void TestMultiplyGeneric()
        {
            var a = new System.Activities.Expressions.Multiply<long, long, long>()
            {
                Left = 100,
                Right = 200,
            };

            var r = WorkflowInvoker.Invoke(a);
            Assert.Equal(20000L, r);

        }

        /// <summary>
        /// Multiply want all types the same.
        /// </summary>
        [Fact]
        public void TestMultiplyGenericThrows()
        {
            Assert.Throws<InvalidWorkflowException>(() =>
            {
                var a = new System.Activities.Expressions.Multiply<int, int, long>()
                {
                    Left = 100,
                    Right = 200,
                };

                var r = WorkflowInvoker.Invoke(a);
            });

        }

        /// <summary>
        /// Multiply<> want all types the same. It seem either bug or design defect. If not bug, then it is better of to have 1 generic type.
        /// </summary>
        [Fact]
        public void TestMultiplyGenericThrows2()
        {
            Assert.Throws<InvalidWorkflowException>(() =>
            {
                var a = new System.Activities.Expressions.Multiply<int, long, long>()
                {
                    Left = 100,
                    Right = 200L,
                };

                var r = WorkflowInvoker.Invoke(a);
            });

        }

备注

正如您从测试用例中看到的那样,这个泛型类实际上要求 TLeft、TRight 和 TResult 是同一类型。并且这种约束显然没有文档记录。我不确定这是否是一个错误或设计使然。如果是设计使然,那么最好为 Left、Right 和 Result 使用 1 个泛型类型。

处理多个输出的方式

现在这里有一个需求,即编写一个 CodeActivity 派生类,该类可以读取 DateTime 并输出 Year、Month、Day。以下代码片段显示了处理多个输出的方式

    public class DateToYMD1 : CodeActivity //classic way of return results
    {
        protected override void Execute(CodeActivityContext context)
        {
            var v= Date.Get(context);
            Y.Set(context, v.Year);
            M.Set(context, v.Month);
            D.Set(context, v.Day);
        }

        public InArgument<DateTime> Date { get; set; }

        public OutArgument<int> Y { get; set; }

        public OutArgument<int> M { get; set; }

        public OutArgument<int> D { get; set; }
    }

    public class YMD
    {
        public int Y { get; set; }

        public int M { get; set; }

        public int D { get; set; }
    }

    public class DateToYMD2 : CodeActivity<YMD> //strongly typed with a data container class defined in advance
    {
        protected override YMD Execute(CodeActivityContext context)
        {
            var v = Date.Get(context);
            return new YMD()
            {
                Y = v.Year,
                M = v.Month,
                D = v.Day
            };
        }

        public InArgument<DateTime> Date { get; set; }

    }

    public class DateToYMD3 : CodeActivity<Tuple<int, int, int>> //strongly typed with Tuple
    {
        protected override Tuple<int, int, int> Execute(CodeActivityContext context)
        {
            var v = Date.Get(context);
            return new Tuple<int, int, int>(v.Year, v.Month, v.Day);
        }

        public InArgument<DateTime> Date { get; set; }
    }


        [Fact]
        public void TestDateToYMD1()
        {
            var a = new DateToYMD1()
            {
                Date = new DateTime(2016, 12, 23)
            };
            var dic = WorkflowInvoker.Invoke(a);
            Assert.Equal(2016, (int)dic["Y"]);
            Assert.Equal(12, (int)dic["M"]);
            Assert.Equal(23, (int)dic["D"]);

        }

        [Fact]
        public void TestDateToYMD2()
        {
            var a = new DateToYMD2()
            {
                Date = new DateTime(2016, 12, 23)
            };
            var r = WorkflowInvoker.Invoke(a);
            Assert.Equal(2016, r.Y);
            Assert.Equal(12, r.M);
            Assert.Equal(23, r.D);
        }

        [Fact]
        public void TestDateToYMD3()
        {
            var a = new DateToYMD3()
            {
                Date = new DateTime(2016, 12, 23)
            };
            var r = WorkflowInvoker.Invoke(a);
            Assert.Equal(2016, r.Item1);
            Assert.Equal(12, r.Item2);
            Assert.Equal(23, r.Item3);
        }

处理多个输出的 3 种方式

  1. 词典
  2. 复合类型
  3. 元组

对于多个强类型输出,显然您需要一个复合类型。如果您不想仅仅为了在 CodeActivity 派生类中进行强类型输出而定义一个复合类型,您可以考虑使用 Tuple。

 

关注点

在 .NET Framework 的主要基础中,与 Windows Communication Foundation 和 Windows Presentation Foundation 相比,人们谈论 Windows Workflow Foundation 相对较少。我想可能不需要 WF 的功能的业务应用程序类型不多。您怎么认为?

请保持关注,本系列将会有更多文章。

© . All rights reserved.