Freigeben über


(WF4) Less Known Features - Declarative Expressions using Activities, and ExpressionServices.Convert

If you’re exclusively using the Visual Studio workflow designer to design activities, you might go for a very long time (or forever) without discovering a particular System.Activities namespace, which is System.Activities.Expressions.

The first reason you might never discover them is that none of the activities here are visible in the Visual Studio workflow toolbox by default. The second reason you might not discover them is that the activities here are actually kind of hard to use from within Workflow Designer. Which means they aren’t going to come up in conversation as often with your buddies.

I should restate for clarity - is it possible to use these activities in Visual Studio workflow designer? Yes. Is it recommended to use them in Visual Studio workflow designer? No. The expression activities have bad usability when used in a drag+drop experience in VS.

If all this has just got you really curious, then try the following. Start a new WF console app, right-click your VS toolbox, and create a new category (‘Add Tab’) called ‘Expressions’. Now inside the category, right-click again, and ‘Choose Toolbox Items’.

image

Select the System.Activities Components tab for WF4 activities. Now in the filter, enter the namespace text ‘Expressions’, select everything, and click OK. Now you have expressions.

image

Now honestly, the naming of the generic type parameters in the toolbox here is a bit of fail. Add<T1,T2,T3>. T1, T2, T3? What do they mean? It turns out the MSDN doc has the correct labels for the types: TLeft, TRight, and TResult, but the toolbox wasn’t designed to show generic type names. (Hmm, maybe I should file a bug for that while I think of it.)

If you play around with the activities in VS you’ll find they’re not that easy to use for a couple reasons:

1) Too many generic type parameters
2) You require lots of variables to do anything useful with them! Ugh!
3) It’s much slower to set two input arguments through the property grid than it is to type an expression lhs && rhs

Hence the activities are not in the toolbox by default, and hence you should probably take them out again, right now, and resist the temptation to use them!
Wait – so these activities that are so hard to use - what, one may ask, are these activities actually good for?

Shouldn’t I just use VB expressions, or replace these activities with VisualBasicValue<> and VisualBasicReference<> anywhere I would think of using them, since it is so much easier to edit a VB Expression than one of these activities?

Yes, you should just use VB expressions.

But wait… really? I have heard that compiling VisualBasicValue and VisualBasicReference during CacheMetadata() is much slower than running CacheMetadata() on a tree of declarative expressions.

So how much slower is it? Well, I tried to measure it.
Unfortunately, I had a hard time finding a significant difference in repeated tests. If anything, the declarative approaches seemed slightly slower. (Your Mileage may vary.)

Operation takes: 1.996992 seconds [VB]
Operation takes: 2.0125935 seconds [declarative]

(actually these are times for 20,000 iterations of each sample, so imagine the numbers a little smaller)

I think that probably most of the costs encountered in practice with VB would be one-off hits related to loading the Visual Basic Compiler, and the measurement appears to show it is not a significant issue.

(sample code for testing).

    Variable<string> v1 = new Variable<string>("v1");

    Sequence s1 = new Sequence

    {

        Variables = { v1 },

        Activities =

        {

            new WriteLine

            {

                Text = new VisualBasicValue<string>("\"hello\" & v1")

            }

        }

    };

    WorkflowInspectionServices.CacheMetadata(s1);

 

    Variable<string> v2 = new Variable<string>("v2");

    Sequence s2 = new Sequence

    {

        Variables = { v2 },

        Activities =

        {

            new WriteLine

            {

                Text = new InArgument<string>(

                    new InvokeMethod<string>

                    {

                        TargetType = typeof(String),

                        MethodName = "Concat",

                        Parameters =

                        {

                            new InArgument<string>(new Literal<string>("hello")),

                            new InArgument<string>(new VariableValue<string>(v2))

                        },

                    }),

            }

        }

    };

    WorkflowInspectionServices.CacheMetadata(s2);

The declarative authoring user experience in code looks pretty painful right?...
The second version was
1) more actual text to write
2) more looking up stuff on MSDN
3) more actual compile errors (among other things, caused by me mixing up ArgumentValue, ArgumentReference, and VariableValue)

However, there is a way to erase much of that authoring pain, which is to use the ExpressionServices.Convert API (or ExpressionServices.ConvertReference when appropriate).

Example:

 

    Variable<string> v3 = new Variable<string>("v3");

    Sequence s3 = new Sequence

    {

        Variables = { v3 },

        Activities =

        {

            new WriteLine

            {

                Text = ExpressionServices.Convert<string>(

                    (env) => string.Concat("hello", v3.Get(env)))

            }

        }

    };

    WorkflowInspectionServices.CacheMetadata(s3);

This brings the ease of authoring at least up to par with Visual Basic expressions. Of course the call to ExpressionServices.Convert itself isn’t free - my quick and dirty tests show a 1.5 times performance hit.

So far I’m feeling quite unconvinced that perf is a reason to use declarative expression authoring. But we’ve only looked at CacheMetadata() perf. What about runtime perf?
For this measurement we’re going to change the scenario slightly and run it in two variations:

Change: Replace WriteLine with Assign
Variation A: Run InvokeWorkflow on a fresh workflow each time (also rebuilding the workflow).
Variation B: Run CacheMetadata just once, and invoke a single workflow lots of times (in accordance with Ron’s workflow performance tips)!

Variation A Results
VB takes: 7.7691984 seconds
System.Activities.Expressions takes: 2.6053336 seconds
ExpressionServices.Convert takes: 4.0874096 seconds

Variation B Results
VB takes: 0.4212216 seconds
System.Activities.Expressions takes: 0.6084312 seconds
ExpressionServices.Convert takes: 0.6552336 seconds

(20k iterations each)

And on the rerun we get a tie for using ExpressionServices.Convert or not (since it’s just run once, not 20,000 times).

VB takes: 0.4212216 seconds
System.Activities.Expressions takes: 0.6396328 seconds
ExpressionServices.Convert takes: 0.6396328 seconds

Interestingly, it seems that for some reason all the calling of CacheMetadata on VB expressions really is slower in this scenario. Maybe we are now doing enough work to blow something important out of a cache somewhere? I really have no idea. However, in terms of just runtime cost, it appears that VB expressions can be even faster than declarative expression activities.

So there you have it. Declarative Expression Authoring using System.Activities.Expressions. Possible, but not necessarily better.
Worth remembering is that it’s probably a lot easier to just stick to VisualBasic expressions or use ExpressionServices.Convert.