In earlier chapters, we alluded to rules functionality that would let more of the logic of WF programs be expressed declaratively to allow WF program authors to rely on code minimally or not at all. The rules capabilities of WF greatly strengthen the case to be made for fully declarative WF programs, for which markup alone carries sufficient expressive power to capture real-world processes. The following discussion of rules capabilities in WF (essentially, the types defined in the System.Workflow.Activities.Rules namespace) is predicated upon a basic understanding of CodeDOM because rule conditions and actions are generally specified using System.CodeDom expressions and statements. A broader discussion of rule-based systems is beyond the scope of this book. Inferencing enginesboth forward-chaining and backward-chaining varietiesand the algorithms that underlie them are an active area of work in academia as well as in industry. Software products that include inferencing engines are in the marketplace today. WF provides simple but powerful ways of melding program logic that is expressed using rules with the activity-oriented approach, which is at the heart of the WF programming model. This section provides a brief but pragmatic exploration of the rules capabilities of WF. We encourage you to consult the .NET Framework documentation and SDKas well as industry literatureto develop your expertise in this important domain. ConditionsBefore we get to WF rules, we must begin with conditions. A condition is an expression that, when evaluated, yields a Boolean value. We employed a condition in Chapter 4 as part of the activity metadata of our While composite activity. The concept of a condition is a fundamental abstraction that you will find useful again and again as an activity developer, wherever you require conditional logic to be expressed by the author of the WF program. Branching, looping, conditional transitions, and conditional (early) completion are a few examples of how conditions can be utilized by activities. System.Workflow.ComponentModel.ActivityCondition is the base type for conditions that are associated with activities. ActivityCondition is an abstract class with a single method, Evaluate, as shown in Listing 8.1. The first parameter of the Evaluate method is the activity that is associated with the condition; the second parameter is a System.IServiceProvider that can be used by derivatives of ActivityCondition to obtain services required to complete the evaluation of the condition. Listing 8.1. ActivityCondition
As an activity developer, when you require a condition property for an activity you are developing, you can define a property of type ActivityCondition. Listing 8.2 defines a new composite activity called Conditional, with such a property. Listing 8.2. Conditional Activity
With this implementation of the Condition property, users of the Conditional composite activity are free to utilize whatever derivative of ActivityCondition is most appropriate for their WF program; meanwhile, the Conditional activity is shielded from the details of this choice because the activity execution logic (of Conditional) will only need to invoke the condition's Evaluate method. WF includes two condition types that derive from ActivityCondition: System.Workflow.Activities.CodeCondition and System.Workflow.Activities.Rules.RuleConditionReference. We will look at these two classes next. Of course, you are also free to develop a custom condition type; so long as your custom condition type derives from ActivityCondition, WF program authors will be able to use it interchangeably with the two condition types that WF provides. Code ConditionsThe purpose of CodeCondition is to allow a WF program author to express conditional logic in terms of C# or VB.NET code compiled into a delegate. CodeCondition is shown in Listing 8.3. Listing 8.3. CodeCondition
The CodeCondition.Condition event is raised during the execution of the Evaluate method. The result of condition evaluation (the return value of Evaluate) will be the value of the Result property of the System.Workflow.Activities.ConditionalEventArgs (see Listing 8.4) at the conclusion of the event handler invocation. Listing 8.4. ConditionalEventArgs
A CodeCondition is expressed in XAML as shown in Listing 8.5 (using the Conditional activity we defined previously as the context for the example). Listing 8.5. Condition Expressed in Terms of Imperative Code Using x:Code
In the preceding simple example, the condition always evaluates to TRue, but in general your code can execute whatever logic (and whatever examination of WF program instance state) is required to determine the outcome of condition evaluation. The XAML program in Listing 8.5 uses the x:Code feature of XAML in order to directly embed code within a XAML document. This is entirely equivalent to (in other words, the result of compilation is the same as) separating the code into a separate file that defines a partial type, just as we have shown in previous examples. For this reason, use of x:Code requires use of x:Class. There is a validator component associated with the CodeCondition type, which ensures that the Condition event is always handled. The event handler subscription must be established at design-time and therefore is part of the metadata of the WF program. Declarative ConditionsThe second derivative of ActivityCondition provided by WF is System.Workflow.Activities.Rules.RuleConditionReference. The purpose of RuleConditionReference is to allow a WF program author to express conditional logic without writing any imperative code. RuleConditionReference is shown in Listing 8.6. Listing 8.6. RuleConditionReference
RuleConditionReference is just a simple level of indirection. Its ConditionName property names a System.Workflow.Activities.Rules.RuleCondition that is found within an associated System.Workflow.Activities.Rules.RuleDefinitions object, which acts as a container for all of the declarative conditions and rules associated with a WF program. This is depicted in Figure 8.1. Figure 8.1. A RuleConditionReference points to a RuleCondition.
The RuleDefinitions type is shown in Listing 8.7. The Conditions property of RuleDefinitions is where we find the actual condition named by a RuleConditionReference; we will discuss the RuleSets property of RuleDefinitions in the next section. Listing 8.7. RuleDefinitions
A RuleConditionReference is expressed in XAML like this (using the Conditional activity we defined previously): <Conditional> <Conditional.Condition> <wf:RuleConditionReference ConditionName="LoopCondition"/> </Conditional.Condition> ... </Conditional> The RuleDefinitions data for the WF program (which, when authoring a WF program, is specified in a separate file that by convention has an extension of .rules) will contain the actual condition definition, as shown in Listing 8.8. Listing 8.8. CodeDOM Conditional Expressions Serialized to XAML
The RuleDefinitions.Conditions property is a collection of objects of type System.Workflow.Activities.Rules.RuleCondition. RuleCondition is the base class for declarative condition implementations. WF provides a single derivative of RuleCondition, System.Workflow.Activities.Rules.RuleExpression-Condition. Shown in Listing 8.9, RuleExpressionCondition is simply a wrapper around a System.CodeDom.CodeExpression. Specifically, it is XAML serialization of the CodeDOM expression tree. In the preceding XAML snippet, which is functionally equivalent to the CodeCondition implementation we provided earlier (because it always evaluates to TRue), we use a System.CodeDom.CodePrimitiveExpression to express the constant System.Boolean value TRue. The markup is verbose, but it is actually specifying the initialization of a very simple object: CodePrimitiveExpression expression = new CodePrimitiveExpression(true); RuleExpressionCondition condition = new RuleExpressionCondition("LoopCondition", expression); RuleDefinitions definitions = new RuleDefinitions(); definitions.Conditions.Add(condition); Listing 8.9. RuleExpressionCondition
To summarize, condition properties on activities are of type ActivityCondition. A condition can be implemented as an event handler in terms of imperative code, or declaratively using markup that specifies CodeDOM expressions. You may also develop custom condition types that derive from ActivityCondition. In general, activities can expose properties that represent either expressions in textual form or compiled code (delegates). We just saw how CodeDOM expressions, serialized as XAML, can be associated with activities but it is also possible to associate other expression representations with activities, including XPath or System. Expression based expressions. If activities in a WF program do not rely upon compiled code, but instead are provided with any necessary expressions in some textual form, then execution of the program can occur without a compilation step. RulesIn the previous section, we learned that a RuleConditionReference is a markup-based alternative to a CodeCondition. More broadly, though, rules are a markup-based alternative to expressing program logic in code. A rule in WF is just a statement (requiring no compilation) that is expressed according to the following pattern: IF condition THEN actions ELSE other-actions A rule set is a collection of rules that act in concert to form a single computational unit. Expressing program logic as a set of rules instead of as compiled code has two important advantages. First, by embracing rules, you can build design environments for WF programs that do not require the author to do any coding. Second, by expressing logic declaratively (rules are just part of the metadata of a WF program) you allow that logic to be modified on the fly, in the context of a single WF program instance. Making changes to running instances of WF programs is a topic we will discuss later in this chapter. For the WF program developer, the System.Workflow.Activities.PolicyActivity activity type is the gateway to rules functionality. A policy is a wrapper around a rule set. The PolicyActivity type is shown in Listing 8.10. Listing 8.10. PolicyActivity
When a PolicyActivity executes, it simply executes the rule set that is named by its RuleSetReference property. Thus, the purpose of PolicyActivity is to allow the execution of a rule set to be modeled as an activity within any WF program. System.Workflow.Activities.Rules.RuleSetReference, shown in Listing 8.11, names a rule set that is found in the collection of rule sets held by the Rule-Definitions object (refer to Listing 8.7) associated with the WF program. This is depicted in Figure 8.2. Listing 8.11. RuleSetReference
Figure 8.2. A RuleSetReference points to a RuleSet.
A WF rule set holds a collection of System.Workflow.Activities. Rules.Rule objects. The System.Workflow.Activities.Rules.RuleSet type is shown in Listing 8.12. Listing 8.12. RuleSet
The Rule type is shown in Listing 8.13. A WF rule is a condition implementationof type RuleCondition, discussed earlierplus a set of actions (the Rule.Then-Actions property) to be taken if the condition evaluates to true. A WF rule can also specify a set of actions (the Rule.ElseActions property) to be taken if the condition evaluates to false. Listing 8.13. Rule
Thus, a WF rule is essentially a construction that takes the following form: if (rule.condition == true) execute rule.then-actions else execute rule.else-actions As a simple illustration of the concept of a rule, consider the following code snippet which, functionally, does exactly what a single rule can do: if (package.Urgency == "High") { package.ShippingType = "Overnight"; DoSpecialPreparation(package); } else { package.ShippingType = "Ground"; } The (package.Urgency== "High") expression is a condition, and each of the statements within the first set of {} braces is an action to be performed if the condition evaluates to TRue. The statements within the second set of {} braces are the actions to be performed if the condition evaluates to false. A rule set, as we know, is a collection of rules. Because the rules in a rule set can influence one another (by modifying shared data), there are several different ways in which a rule set can be executed. Before we discuss the choices you have that govern WF rule set execution, though, let's take a look at the kinds of actions that can be associated with a rule. The System.Workflow.Activities.Rules.RuleAction type is the base class for rule actions, and WF provides three derivatives, RuleHaltAction, Rule-UpdateAction, and RuleStatementAction, all of which are also defined in the System.Workflow.Activities.Rules namespace. If you use a RuleHaltAction in a rule, execution of this action will cause the execution of the current rule set to immediately end. RuleUpdateAction is used to achieve explicit chaining of rules, which will be discussed shortly. Essentially, execution of this kind of action explicitly tells the rules engine that a specific piece of data (indicated by the Path property of RuleUpdateAction) should be considered to have had its value changed as a consequence of the execution of that action. RuleStatementAction is the general-purpose action type. A RuleStatement- Action wraps a System.CodeDom.CodeStatement, which is executed when the action executes. Among other things, a RuleStatementAction can be used to set the value of a field or property of a WF program (or of an activity within the program), invoke a method available on the WF program (or on an activity within the program), or invoke a static method. Essentially, a RuleStatementAction specifies what otherwise might be expressed as a line of code. Rule Set ExecutionRules in a rule set are evaluated one at a timeby default, in priority order according to the value of Rule.Priorityuntil there are no more rules to evaluate, but there a couple of ways in which you can influence the manner in which a rule set is executed. The ChainingBehavior property of RuleSet indicates whether the rule set should execute without chaining, with explicit chaining only, or with full chaining. The kind of chaining we are referring to here is called forward chaining because the basic idea is that the execution of a rule can change some data that then prompts reevaluation of other (previously evaluated) rules in the rule set whose conditions depend upon that data. If a rule set executes without chaining, each of its rules is evaluated once. The order of evaluation is determined by the value of the Priority property of each Rule object. If two or more rules carry the same priority, their ordering is not guaranteed. Because you will typically be using actions of type RuleStatementAction in your rule set, this option basically amounts to execution of an ordered list of CodeDOM expressions. If a rule set executes with explicit chaining only, the execution of an action of type RuleUpdateAction (which indicates the data that should be considered changed) can cause chaining to occur. Specifically, if a rule "E" modifies data that is read by the condition of a previously evaluated rule "B" (a rule with higher priority), then "B" will be the next rule (after "E") to be evaluated. If a rule set is configured to execute with full chaining, then, in addition to chaining via RuleUpdateAction, chaining can occur implicitly (when the WF rules engine infers that a rule condition's dependent data has changed) or based upon the presence of CLR attributes that serve as hints to the rules engine about when reevaluation of specific rules is needed. To achieve implicit chaining, the WF rules engine parses the CodeDOM statements underlying each rule's RuleCondition along with each rule's set of RuleStatementAction objects so that it knows what data that rule depends upon and what data that rule changes. To realize chaining based upon CLR attributes, you simply decorate methods (that are called by actions of type RuleStatementAction or by the code expression of a RuleCondition) to indicate what data that method reads, what data it writes, and what other methods it calls. The RuleReadAttribute, RuleWriteAttribute, and RuleInvokeAttribute types are defined (in the System.Workflow.Activities.Rules namespace) for this purpose. By default, parameters of a method are assumed to be read by the method (unless they are out or ref parameters in which case they are assumed to be written). As an example of a rule set that uses forward chaining, consider the following set of rules: Rule1 (Priority=3) if (x==2) then z++ else y+=4 Rule2 (Priority=2) if (y==6) then z++, x++ else x=0 Rule3 (Priority=1) if (z==5) then output="red" else output="green" Let's evaluate this rule set (using forward chaining) with the following input data: x = 1 y = 2 z = 3 output = "" The rule evaluation proceeds as follows: Rule1 (else-actions are executed, y is now 6) Rule2 (then-actions are executed, z is now 4, x is now 2) Rule1 since x changed (then-actions are executed, z is now 5) Rule3 (then-actions are executed, output is now "red") To familiarize yourself with the mechanism of rule evaluation, try to work out how rule evaluation unfolds for different sets of input values. There is one additional lever for controlling the execution behavior of a rule set, and that is the ReevaluationBehavior property of Rule. If this property carries a value of Never, reevaluation of the rule will never happen once the ThenActions (or the ElseActions, if present) of such a rule has executed. If the ReevaluationBehavior property carries a value of Always, there is no restriction on how many times the rule can be reevaluated. |