I have implemented a WF4 activity. It has some required InArguments, one required InOutArgument and one optional OutArgument.
Since the activity was implemented in xaml (i.e. with Workflow Designer), I had to search for the info how you can mark attributes as 'required' in xaml (analogon to [RequiredArgument]
in C#) and found following link how to do it:
http://msdn.microsoft.com/en-us/library/ee358733(v=vs.100).aspx
<x:Property Name="Operand1" Type="InArgument(x:Int32)">
<x:Property.Attributes>
<RequiredArgumentAttribute />
</x:Property.Attributes>
</x:Property>
This works fine for all InArguments. But when implementing the tests for it, I found out that it doesn't work correctly with InOutArguments. If I run my xaml activity with WorkflowInvoker.Invoke
in my test, without supplying any parameters, there comes an ArgumentException
that complains about all required InArguments, but not about the required InOutArgument. If I run the activity with all required InArguments, but without the required InOutArgument, no ArgumentException
will be thrown.
Could this be a bug in Workflow Foundation?
An interesting thing is, that if I use this activity in a workflow without supplying any parameters, I get this red exclamation mark in Workflow Designer, that tells me which parameters need some input. And here the InOutArgument is mentioned, which is the expected behavior.
The Workflow engine checks the RequiredArgumentAttribute for one activity calling another. You can see this in action by creating a CodeActivity. Using breakpoints, you can watch with the debugger stop in the CacheMetadata method but an exception is thrown before the Execute method is called.
But, as you pointed out, using a WorkflowInvoker to invoke an activity with InOut/Out arguments directly does not cause an exception for InOut/Out required arguments. The RequiredArgumentAttribute requires that the argument have a binding. It does not require that the argument has a value. This can be seen by binding a reference variable with a null value. The WorkflowInvoker class automatically binds to InOut/Out arguments in order to capture the output values for its return IDictionary object.
In order to write a unit test for required InOut arguments, you have to create an activity that will bind itself to the activity you are testing. The following snippet is code I recently wrote for this exact scenario. I am using the WorkflowInvokerTest class from CodePlex to encapsulate the WorkflowInvoker.
public abstract class ActivityTest
{
private class ArgumentTester : NativeActivity
{
public Collection<Variable> variables = new Collection<Variable>();
public Activity Test;
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
metadata.AddImplementationChild(Test);
foreach (var item in variables)
{
metadata.AddImplementationVariable(item);
}
}
protected override void Execute(NativeActivityContext context)
{
context.ScheduleActivity(Test);
}
}
protected WorkflowInvokerTest host;
protected void TestForRequiredArgument(string argName)
{
var d = (IDictionary<string, object>)host.InArguments;
d.Remove(argName);
try
{
dynamic activityToTest = System.Activator.CreateInstance(host.Activity.GetType());
ArgumentTester tester = new ArgumentTester
{
Test = activityToTest
};
foreach (var item in d)
{
Type t = typeof(Variable<>).MakeGenericType(item.Value.GetType());
Variable v = (Variable)Activator.CreateInstance(t, item.Key + "_Var", item.Value);
tester.variables.Add(v);
var prop = host.Activity.GetType().GetProperty(item.Key);
object arg = Activator.CreateInstance(prop.PropertyType, v);
prop.SetValue(activityToTest, arg);
}
var h = new WorkflowInvokerTest(tester);
h.TestActivity();
Assert.Fail("An exception should have been thrown.");
}
catch (InvalidWorkflowException ex)
{
Assert.IsTrue(ex.Message.Contains("'" + argName + "'"));
}
finally
{
host.Tracking.Trace();
}
}
}
And then I write a test like this:
[TestClass]
public class WageTest : ActivityTest
{
[TestInitialize]
public void InitializeTest()
{
host = new WorkflowInvokerTest(new WageActivity());
host.InArguments.Wage = 2000;
host.InArguments.IsFifthQuarter = false;
}
[TestMethod]
public void WageArgumentIsRequired()
{
base.TestForRequiredArgument("Wage");
}
[TestMethod]
public void IsFifthQuarterArgumentIsRequired()
{
base.TestForRequiredArgument("IsFifthQuarter");
}
//...
}
It could be cleaned up a bit with generics. I'm still working on that, but you get the idea.