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:
- Visual Studio 2015 Update 1 or Visual Studio 2013 Update 3
- xUnit (included)
- EssentialDiagnostics (included)
- Workflow Persistence SQL database, with default local database WF.
Examples in this article are from a test classe: Basic.
CodeActivity
Classic Way of Output
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; }
}
[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,
};
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.
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; }
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()
{
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.
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)
{
}
}
[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>.
[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);
}
[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);
});
}
[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
public class DateToYMD1 : CodeActivity
{
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>
{
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>>
{
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:
- Dictionary
- Composite type
- 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.