Search code examples
xamlworkflow-foundation-4

RequiredArgumentAttribute doesn't work correctly for InOutArguments?


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.


Solution

  • 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.