Click here to Skip to main content
15,884,066 members
Articles / Programming Languages / C#

Learn Windows Workflow Foundation 4.5 through Unit Testing: CodeActivity

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
20 Mar 2016CPOL3 min read 13.4K   4   2
Workflow CodeActivity

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 some wet materials for studying.

Introduction

While WF provides rich functions and features, for .NET developers, .NET developers may mostly crunch with WF for writing CodeActivity and utilizing them in WF applications.

I would skip what you have read in MSDN and tutorials currently available. And this article is focused on giving code examples to strengthen your memory of designing activities and using them.

This is the first article in the series.

Other articles in this series:

Learn Windows Workflow Foundation 4.5 through Unit Testing: InvokeMethod and DynamicActivity

Using the code

Source code is available at https://github.com/zijianhuang/WorkflowDemo

Prerequsites:

  1. Visual Studio 2015 Update 1 or Visual Studio 2013 Update 3
  2. xUnit (included)
  3. EssentialDiagnostics (included)
  4. Workflow Persistence SQL database, with default local database WF.

Examples in this article are from a test classe: Basic.

CodeActivity

Classic Way of Output

C#
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);
    }

 

And you can have multiple properties of OutArgument each of which could be accessed through the dictionary returned by WorkflowInvoker.Invoke().

Remarks:

You might notice that the InArgument<T> properties are assigned with values/expressions of T, and .NET compiler and runtime will create an InArgument wrapper around each value/expression assigned.

 

Strongly typed output in .NET 4

Obviously accessing output through a dictionary of objects is not appealing to developers who get used to strongly typed data. In .NET 4, WF supports strongly typed output through the CodeActivity<TResult> class.

C#
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));
    }

Although in the example there is an OutArgument property, it is useless since there's no interface to access in codes, though the Workflow Designer could access it. It is redundant. So in production codes, Result is enough.

Required Arguments

The InArguments are decorated by RequiredArgumentAttribute, thus they must be assigned otherwise the WF runtime will throw ArgumentException when validating the inputs.

Overloaded Groups

Sometimes you want to have 2 groups of InAgruments, but only either group should be assigned, so you may have overloaded groups.

C#
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));
    }

 

The Built-in Multiply Activity

The Multiply class above is for demo purpose, and in fact .NET Framework 4 has provided Multiply< TLeft, TRight, TResult>.

C#
[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);
    });

}

Remarks:

As you can see from the test cases, this generic class actually requires that TLeft, TRight and TResult are the same type. And such constraint is apparently undocumented. I am not sure if this is a bug or by design. If it is by design, then it may be better to have 1 generic type for Left, Right and Result.

Ways of Handling Multiple Outputs

Now here's a requirement of writing a CodeActivity derived class that can read DateTime and output Year, Month, Day. The following code snippets show ways of handling multiple outputs

C#
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 ways of handling multiple outputs:

  1. Dictionary
  2. Composite type
  3. Tuple

For multiple strongly typed outputs, obviously you need a composite type. If you don't want to define a composite type just for the sack of strongly typed output in a CodeActivity derived class, you may consider using Tuple.

 

Points of Interest

Among major foundations in .NET Framework, Windows Workflow Foundation is relatively less being talked about, comparing with Windows Communication Foundation and Windows Presentation Foundation. I guess there exist not many types of business applications that need the power of WF. How do you think?

Please stay tuned, and there will be more articles in this series.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Australia Australia
I started my IT career in programming on different embedded devices since 1992, such as credit card readers, smart card readers and Palm Pilot.

Since 2000, I have mostly been developing business applications on Windows platforms while also developing some tools for myself and developers around the world, so we developers could focus more on delivering business values rather than repetitive tasks of handling technical details.

Beside technical works, I enjoy reading literatures, playing balls, cooking and gardening.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Gordon_H4-Jan-17 10:53
Gordon_H4-Jan-17 10:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.