Custom Rule Actions

This sample shows the definition and use of several custom RuleActions in the Windows Workflow Foundation (WF) rules engine.

Author of the original sample: Jurgen Willis

This document is for informational purposes only.  Microsoft makes no warranties, either express or implied, as to the information in this document. The entire risk of the use or the results from the use of this document remains with the user. 

© 2006 Microsoft Corporation.  All rights reserved.

 

Content

Overview

In the Windows Workflow Foundation rules model, a RuleSet contains a collection of Rule objects.  Each Rule object contains a RuleCondition which is evaluated to determine if the Rule is true or false.  Depending on the result, a list of Then actions or Else actions is executed.  These actions are expressed using the RuleAction Type, or more precisely
derivatives of the RuleAction Type, which is abstract. 

With WF we provide three RuleAction subclasses for modeling different kinds of behaviors:

A subset of the rules object model is shown in the Readme.mht file that is included in the sample folder.

It is also possible for custom RuleActions to be defined by deriving from the RuleAction type.  This sample shows three such custom actions that:

1) Write text to the Console

2) Execute a second ruleset

3) Queue an activity for execution

Each is described later in this document.

It is reasonable to ask “why not just use a method on the workflow to define these behaviors” – the answer is that you would need to define this method on every workflow you create (or create a new workflow type).  You might then propose using a static method on a referenced Type.  The answer here is that defining it as a custom action allows the action to:


To use a custom action in the RuleSet editor, simply type in the action class’s constructor, passing it the constructor parameters.  As long as the assembly with the custom action is referenced by the workflow project, the parser will automatically detect the existence of the custom action. 

 

Sample Usage:

Each of the custom actions comes in its own solution , which includes the derived RuleAction type, as well as a sample workflow project which uses the custom action in a ruleset. (look under these three folders: CustomActivityAction, CustomLogAction and CustomRuleSetAction)

Simply compile the solution and press F5 or Ctrl-F5 in Microsoft Visual Studio to run the workflow application (or run the application exe in the workflow project’s \bin\Debug folder).

 

Building a Custom RuleAction:

The requirement of a custom action is that it derives from the RuleAction Type and overrides RuleAction’s abstract
methods, which consist of:

In addition, a custom action should provide a parameter-less constructor to support deserialization.  If the action requires any inputs, a second constructor should be provided that passes these inputs.  The inputs should be of type
CodeExpression. 

Finally, the action should override the ToString method to support use of the action in the RuleSet editor. The ToString method implementation should rebuild the action’s constructor in string form.  This is important for the action to roundtrip effectively in the editor.  In other words, when the action is retrieved for display in the editor, its ToString method is called.  The string representation will then be re-parsed when the rule is edited/saved, and as described above, the text representation must be the action‘s constructor for it to be accurately parsed. 

Sample RuleActions

The three custom actions in this sample are described below.  They are presented in order of increasing complexity.

 

Log Action

This custom RuleAction executes a Console.WriteLine() call on the text that is passed to it.

The action has a parameter-less constructor, as required, as well as a constructor that takes a CodeExpression instance.  The CodeExpression instance is evaluated at runtime to determine the text to be displayed.

The Validate method of the action checks that the expression isn’t null and then validates the passed CodeExpression instance.  This is done using static methods on the RuleExpressionWalker class, which is used extensively in defining custom actions and expressions.  The Validate method returns a Boolean result indicating if validation was successful or not.  Validation errors are added to the passed in RuleValidation instance.

The Execute method evaluates the CodeExpression instance using the RuleExpressionWalker to get the text for display.  Note that the CodeExpression could represent many different types of expressions including a CodePrimitiveExpression containing a literal value, a CodePropertyReferenceExpression accessing an object’s property, or a CodeMethodReferenceExpression that returns a string result.

In the GetSideEffects method the code simply turns around and calls AnalyzeUsage on the CodeExpression to determine how that expression uses the data.  The action does not update any data, so only the expression’s behavior is important. 

The CustomLogAction project with the custom action is then referenced by a Sequential Workflow Console Application, “SampleWorkflow”, which uses the log action.  The text to be displayed, “Hello world!”, is passed in from the Program.cs file as the “DisplayText” parameter to the workflow (this flows to the DisplayText property on the workflow).  The workflow has a Policy activity with a single rule that calls the Log action, passing in a reference to the displayText field on the workflow (this will be parsed into a CodeFieldReferenceExpression pointing to the field).

 

Execute RuleSet Action

When executed in a rule, this action executes another ruleset.  It takes a single CodeExpression that specifies the name of the ruleset to be executed.

In the Validate method, the CodeExpression is checked to make sure that it is a CodePrimitiveExpression – i.e. a literal value.  It is not strictly required that the passed in expression be a literal string value, but doing so allows for design time validation to ensure that the ruleset to be called is defined and available. 

There are few nuances to checking that the called ruleset is available at design time.  A GetCalledRuleSet method is defined that attempts to access the ruleset based on the called ruleset’s name.  A handle to a workflow instance is not passed to the action, but the Type that the rules are authored against is (via the RuleValidation instance).  Since the Policy activity assumes that the target of the rules is the workflow itself, this provides the Type of the workflow. Using the workflow Type, an instance is created and then the
RuleDefinitions dependency property is accessed on the workflow to try and locate a RuleSet with the name passed into the action (via the CodeExpression on the action’s constructor). 

Note that an instance of the workflow can only be created if an actual compiled Type exists, not a “design time type” which exists prior to compilation.  Therefore, the check can only be made at compile time, it cannot be made during the course of workflow authoring. The implication is that you won’t see smart tag errors on the Policy
activity, but you will see errors when the workflow project is compiled.

In the Execute method, the action gets the ActivityExecutionContext from the passed RuleExecution instance.  It then uses the GetDeclaringActivity method to walk up the activity hierarchy from the executing activity to find the workflow (in some scenarios the Policy activity could actually be contained within another compiled custom activity – the code supports this, but for purposes of this document it is assumed that the Policy
exists directly in a workflow).  From the workflow, the ruleset to be executed is retrieved from the RuleDefinitions property; the called ruleset is then validated and executed.  Note that this sample assumes that the Policy activity which executes the first ruleset is contained directly in a workflow and not another custom activity.  See the next action sample for a demonstration of how to relax this assumption.

The only side effects of this action are the side effects of the ruleset that is called.  Therefore, in the GetSideEffects method the code loops through each rule in the called ruleset and determines the rule’s possible side effects.  The behavior which will result is that after a custom action is executed, the side effects of the called ruleset will be used to determine which rules in the calling ruleset should be reevaluated.

Note that the behavior will be slightly different than that seen within a single executing ruleset.  Within a ruleset, if a rule executes its Then actions, the side effects of those actions are used to determine the forward chaining that should take place on other rules in that ruleset; conversely, if the rule’s Else actions are executed, then the side effects of the Else actions are used.  In other words, only the side effects of actual executed actions are used.   

In the sample workflow associated with this action, there are two Policy activities.  The first is a “dummy” activity that is simply used to access and author the called ruleset – the activity is disabled and does not execute when the workflow runs. The associated ruleset simply updates the “itWorked” field on the workflow to True (the value of the field is written to the console after the workflow completes). 

The second Policy activity executes a ruleset which uses the custom action, passing in the name of the ruleset to be called when the action is executed.

 

Execute Activity Action

This action takes the name of an activity and queues the associated activity for execution.  To do this effectively, this sample requires both a custom action, as well as a custom CompositeActivity.

We’ll look first at the CompositePolicy activity and then at the associated custom RuleAction.

 

CompositePolicy Activity

When the CompositePolicy activity executes it runs a referenced ruleset which will then, based on the defined rules, queue up 0:N child activities of the CompositePolicy, using the custom action. 

The CompositePolicy contains a collection of child activities.  The ActivityPreviewDesigner designer is used to represent the activity in the workflow editor.  The ConditionedActivityGroup (CAG) activity’s designer derives from ActivityPreviewDesigner, so the look and feel of the activity on the workflow canvas will be very similar to the CAG. 
It is important to note, though, that the similarity is only at the designer level – the implementation of this activity is completely separate from the CAG (although the notion of condition-driven activity execution is shared between them).

The CompositePolicy activity has a RuleSetReference property that points by name to a RuleSet instance.  This is similar to the WF Policy activity, which also has a RuleSetReference property, and the behavior is the same.  The RuleSet browser and editor can be launched from the Properties window by clicking on the ellipses in the RuleSetReference property or the ellipses in the RuleSet Definition sub-property, respectively. 

The Execute method on the CompositePolicy activity calls the private ExecuteRuleSet method; this method gets the “declaring activity”, accesses the ruleset from the declaring activity using the name stored in the CompositePolicy’s RuleSetReference property, and executes the ruleset.  The declaring activity is the Type that contains the CompositePolicy activity.  In simple cases this will be the workflow itself.  However, it could also be a custom activity if the CompositePolicy activity was placed in a custom activity, the custom activity was compiled and the custom activity was then used in a workflow. 

As we will see below, executing the ruleset with its use of the CustomActivityAction will (typically) result in child activities of the CompositeActivity being queued for execution.  After the ruleset has been executed the activity checks to see if it has any executing child activities using the ChildIsExecuting method and closes if there are no executing children.

If the custom action queues child activities for execution, then it will also register the CompositePolicy activity for the closed events for those child activities. 

Whenever the CompositePolicy activity receives notification that one of its child activities has completed (via the
IActivityEventListener<ActivityExecutionStatusChangedEventArgs>.OnEvent handler), the code will unregister for the event (as would any composite activity), close the associate child context and check to see if there are any
remaining child activities executing.

 

Custom Action

As with the RuleSet action described above, this action takes a string value, which in this case references
an activity.  Again, this is not absolutely required; the activity to be executed could, for example, be retrieved from a workflow field at runtime.  Using a literal value, however, allows for compile-time validation that the activity exists and is accessible.

In the Validate method, a workflow instance is created in the same manner as in the RuleSet action.  The code then calls GetActivityByName on the workflow to ensure that the activity referenced exists in the workflow.  Furthermore, the code checks that the referenced activity is a direct child of the CompositePolicy activity.

In the action’s Execute method, the code accesses the currently executing activity from the ActivityExecutionContext; this should be the CompositePolicy activity.  A little complexity is also introduced here to account for scenarios where the CompositePolicy activity is compiled inside a custom, parent composite activity.  In the simple case, the CompositePolicy is placed directly in the workflow; this allows the activity template to be located using the activity name passed to the action.  In the case where the CompositeActivity is compiled inside a custom activity, the qualified name must be derived and then used to locate the activity template.

In either case, it is the activity template that is located; this is different from the actual activity instance which will be created and executed.  If an activity will be executed multiple times during workflow execution, cloned instances of the
activity must be created and executed inside their own ActivityExecutionContext.  This is what the Replicator, CAG and While activities do for their child activities.  Please see the WF documentation and samples for more details on this particular topic.

The code then calls CreateExecutionContext to create a new execution context which contains a new, cloned activity instance from the activity template.  The new execution context is then used to register the parent CompositePolicy for the closed event from the child activity and then to execute the child activity. More precisely, the ExecuteActivity call is made on the context which results in the execute call being placed on a queue for processing. 

Actual execution of the child activity is asynchronous and the execute call will not be delivered to the child activity until the rule action completes, the ruleset completes, and the Execute method on the CompositePolicy returns.  The important point here is that the ruleset is queueing activities for execution; the rule and ruleset do not wait for those activities to complete (although the enclosing CompositePolicy activity does wait for the child activities to complete).  Therefore, these actions should be considered as “fire and forget” and are not suitable for scenarios where the rules issue an asynchronous request and require a response before the rules can progress; for example, an InvokeWorkflow activity would be appropriate since no response is required, but use of an InvokeWebService-WebServiceOutput pair of activities – where the web service response is intended to be used in the rules - would not be appropriate. 

The GetSideEffects method simply returns null, indicating that this action has no side effects.  It is possible, of course, that the activity that this action queues for execution does have some side effects, e.g. the activity could set some workflow properties.  However, there is no good way to identify the side effects of these activities. Even if this were possible, it would be a moot point.  Since the activities execute after the ruleset has completed, their side effects would not be able to cause chaining in the ruleset (although their side effects could be observed in a subsequent re-execution of the ruleset).

 

Workflow Sample

In the workflow for this example, the CompositePolicy contains 3 Code activities, “Send Email”, “Mail Introductory Package” and “Schedule Followup” which are intended to mimic these actions in a customer contact scenario.

When the console application is run, Boolean values for the “HighValueCustomer” and “HighProbabilityCustomer” parameters are passed to the workflow. The ruleset associated with the CompositePolicy activity will use the custom action to queue the child activities, based on an evaluation of the parameters passed to the workflow.  After all queued child activities have completed the CompositePolicy activity will complete and the workflow will finish.