There are a few ways to create rules: through the UI provided in Visual Studio (the most common method), by using XML, and programmatically by using standard .NET code.
You can access the rules dialog boxes defined in the Windows Workflow Foundation API from a few predefined points in Visual Studio. The following sections describe these dialog boxes and how you can access and use them.
The Rule Set Editor dialog box (shown in Figure 9-4) provides all the functionality necessary to create new rule sets or modify existing ones. You access this dialog box from the Policy activity’s RuleSetReference property.
Figure 9-4
Keep in mind that just because only one out-of-the-box activity uses the concept of rule sets does not mean that you can’t develop custom activities to use them as well.
You can accomplish quite a few things in this dialog box, including adding new rules to the current set, editing existing rules, and setting rule set chaining options. The list view toward the top of the screen lists all rules currently defined in the set. You can manipulate individual rules by selecting an item in the list and either deleting (by clicking the Delete button) or editing the rule’s properties, which are populated in the area below the list.
The properties area provides essentially everything you need to modify a rule. The rule’s IF, THEN, and ELSE statements are displayed here. In addition, the name, priority, and reevaluation properties can be modified. There is also an Active check box that specifies whether the currently selected rule should be evaluated during the rule set’s evaluation.
One of the nice features of the rules UI in Windows Workflow Foundation is the inclusion of IntelliSense. When a rule’s IF, THEN, or ELSE statements are being edited, contextual hints are given using the workflow’s code-beside class as a baseline. This means that if you type this. in the IF box, you are presented with a list of variables from the workflow definition’s class. For example, if a workflow’s class had a member called order, it would appear in the IntelliSense list as shown in Figure 9-5.
Figure 9-5
Even though there is only one custom field defined in this example, the list is quite long. This is because the list is showing all members of the workflow class and its parent classes.
The declarative Rule Condition Editor, shown in Figure 9-6, allows you to specify simple rule conditions for use in conditional activities such as IfElse, While, and ConditionedActivityGroup. Not surprisingly, there is not a lot to this dialog box, because rule conditions are simply a Boolean expression. However, the UI provides nice features such as IntelliSense and rule validation. The red exclamation icon on the right side of the screen indicates a problem with the current condition. In this case, nothing has been entered in the required editor area.
Figure 9-6
The workflow API exposes the rules UI dialog boxes for use in custom applications. These classes are located in the System.Workflow.Activities.Rules.Design namespace and include RuleCondition Dialog and RuleSetDialog.
The RuleConditionDialog is the UI that allows users to create declarative rule conditions based on the workflow that is passed to its constructor, as shown in the following code listing. Any members that have been defined in the MyWorkflow class are accessible in the rules UI, just as in Visual Studio. After the ShowDialog method is called and the user has created the desired condition, it is accessed with the Expression property. A RuleDefinitions instance is then created and the condition is added after being wrapped in a RuleExpressionCondition object. Finally, the RuleSetDefinition is serialized and written to an XML file.
private void CreateRuleCondition() { MyWorkflow wf = new MyWorkflow(); RuleConditionDialog rdc = new RuleConditionDialog(wf, null); rdc.ShowDialog(); CodeExpression ce = rdc.Expression; RuleDefinitions ruleDefinitions = new RuleDefinitions(); RuleExpressionCondition condition = new RuleExpressionCondition("MyCondition", ce); ruleDefinitions.Conditions.Add(condition); WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer(); serializer.Serialize(new XmlTextWriter(@"C:\rules.xml", Encoding.Default), ruleDefinitions); }
In addition to creating declarative rule conditions, the workflow API provides you with a dialog box where you can create more complex rule sets by using the RuleSetDialog class. The following code listing shows you how to use this class to create a rule set in a custom application:
private void CreateRuleSet() { RuleSet ruleSet = new RuleSet("MyRuleSet"); MyWorkflow wf = new MyWorkflow(); RuleSetDialog rsd = new RuleSetDialog(wf, ruleSet); rsd.ShowDialog(); ruleSet = rsd.RuleSet; RuleDefinitions ruleDefinitions = new RuleDefinitions(); ruleDefinitions.RuleSets.Add(ruleSet); WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer(); serializer.Serialize(new XmlTextWriter(@"C:\rules.xml", Encoding.Default), ruleDefinitions); }
This code first creates an empty RuleSet instance simply to give it a name. The RuleSetDialog is then initialized by passing an instance of the MyWorkflow workflow class. As with the RuleConditionDialog, passing this workflow reference allows the user to access, with IntelliSense, any members defined in the workflow class.
After the rule set has been defined by the user, it is accessible through the RuleSet property. A Rule SetDefinitions class is created to hold the RuleSet object, and then it is serialized to XML on the filesystem.
Even with the rich UI experience provided out of the box with Windows Workflow Foundation, there might be situations when rules need to be created programmatically. Think about a project that requires end users to create rules without the assistance of IT. In many cases, you can just embed the provided dialog boxes in custom applications, but this is not always feasible. If there are very specific guidelines about how business rules can be defined, you may need to write custom code to fulfill the requirements.
The following sections describe how you can define rules in code and how these rules are subsequently evaluated.
Internally, rules are created using classes defined in the .NET CodeDom namespaces (System.CodeDom). The CodeDom is not specific to Windows Workflow Foundation and is used for many different applications within and using .NET.
Essentially, the CodeDom enables you to create code by using code. Think about how you would describe a C# if-else statement using code. First, you need an object representing the if statement that has a construct for producing a value of true or false based on an expression. Next, you need an object representing the actions to be taken if the condition is true and another object representing the else portion of the code. Each discrete code construct and action should be able to be represented by objects that can then be compiled into actual executable code. This is what the CodeDom is for.
Table 9-4 lists the .NET CodeDom expression class types that are used to support the programmatic creation of workflow rules. Keep in mind that the classes listed here represent only a subset of what exists in the CodeDom namespaces. There are plenty of great resources available on the Web that discuss this topic in depth and in a more generic context.
CodeDom Type | Description |
---|---|
CodeAssignStatement | Supports the assignment of a value to a variable. Example: myName = "Todd" |
CodeBinaryOperatorExpression | Allows two code expressions to be operated on with a binary expression such as a comparison or mathematic operation. Example: myAge == 25 |
CodeDirectionExpression | Specifies the direction of a parameter being passed to a method. Example: MyMethod(out int val) |
CodeExpressionStatement | Represents a single expression or line of code. Example: myObject.ToString() |
CodeFieldReferenceExpression | Evaluates to a field reference. Example: this.myVariable |
CodeMethodInvokeExpression | Represents a method call. Example: Console.WriteLine("Hi") |
CodeMethodReferenceExpression | Represents a reference to a method. This object could then be passed to CodeMethodInvokeExpression. |
CodePrimitiveExpression | Represents a single primitive value. Examples: 1, "Hello", false, 10.5 |
CodePropertyReferenceExpression | Evaluates to a property reference. Example: this.MyName |
CodeThisReferenceExpression | Represents a reference to the current class. For example, if you wanted to reference a method on the current, local class, it would look like this: this.PerformValidation(). |
CodeTypeReference | Represents a reference to a .NET type. Example: typeof(System.String) |
CodeTypeReferenceExpression | Represents a reference to a .NET data type. Example: System.String |
Table 9-5 lists the CodeDom classes that allow operations to be performed between two expressions. These operators range from mathematical operations to Boolean comparison operations. All of these operators are values found in the CodeBinaryOperatorType enumeration.
Operators | Description |
---|---|
Add | Used to add expressions. Example: 2+2 |
BitwiseAnd | Performs a bitwise and operation. Example: val1 & val2 |
BitwiseOr | Performs a bitwise or operation. Example val1 | val2 |
BooleanAnd | Represents a Boolean and expression, as used with if statements. Example: val1 == val2&& val3 == val4 |
BooleanOr | Represents a Boolean or expression. Example: val1 == val2|| val3 == val4 |
Divide | Provides the division operator. Example: 10 / 2 |
GreaterThan | Represents the greater than operator. Example: x > 10 |
GreaterThanOrEqual | Represents the greater than or equal to operator. Example: val >= 24 |
IdentityEquality | The identity equality operator. Example: val == anotherVal |
IdentityInequality | The identity inequality operator. Example: val != anotherVal |
LessThan | Represents the less than operator. Example: 5 < count |
LessThanOrEqual | Represents the less than or equal to operator. Example: val <= 40 |
Modulus | Allows the use of the modulus operator. Example: (x%2)==0 |
Multiply | The multiplication operator. Example: 2x2 |
Subtract | The subtraction operator. Example: 40 - x |
ValueEquality | The value equality operator. Example: val == 2 |
In addition to the functionality provided by the CodeDom to create rule expressions, the Windows Workflow Foundation API exposes classes that represent the rule entities. This includes items such as rules themselves, rule sets, and rule definitions that represent the container for all rule-related items in a workflow.
The rule-related objects in the API are located in the System.Workflow.Activities.Rules namespace and enable you to programmatically create rules just as they are created with the Visual Studio APIs. Building from the bottom up, the Rule class exposes properties such as Condition, ThenActions, and ElseActions. The ThenActions and ElseActions properties are collections that can have any number of actions associated with them.
Although the conditions and actions associated with a rule all come from the CodeDom classes, they are wrapped in rulecentric classes when associated with the Rule class. The Condition property is of type RuleExpressionCondition, which takes a CodeExpression as a parameter in one of its constructor’s overloads. The ThenActions and ElseActions collections also wrap CodeExpression classes in a class called RuleStatementAction.
Next in the chain comes the RuleSet class. This class has properties such as ChainingBehavior, Name, Description, and Rules. The ChainingBehavior property takes a value from the RuleChaining Behavior enumeration. The Rules property is a collection of Rule objects that make up the rule set. You programmatically add Rule instances just as you would do visually in the rules UI.
The RuleDefinitions class essentially represents what you would find in the .rules file of a workflow. It is a container for all declarative rule conditions as well as all rule sets associated with a particular workflow. It has two properties: Conditions and RuleSets. As described in the following section, which provides an example of programmatic rule creation, the RuleDefinitions class can be serialized to an XML file, which gives you the equivalent of a .rules file created in Visual Studio.
This section studies some code that programmatically builds a rule set using the rules API and the CodeDom classes. To maintain consistency and so that you can see how the same rules can be modeled in different ways, the code builds the same three rules introduced earlier in the chaining section:
One rule to check whether the renter requires insurance based on the car type and customer age
One rule to provide a premium car upgrade if the customer is classified as Premium
One rule to set the required gas level based on the option chosen by the renter
In addition to building the rules and rule set, the following code serializes the RuleDefinitions object to an XML file. The output XML is just as good as the XML created by the rules UI and can be used in the same way.
public static class RulesHelper { // the following group of static members is simply obtaining references to // fields, properties, and types which will be used throughout the code private static CodeThisReferenceExpression thisReference = new CodeThisReferenceExpression(); private static CodeFieldReferenceExpression rentalFieldReference = new CodeFieldReferenceExpression(thisReference, "rental"); private static CodePropertyReferenceExpression customerFieldReference = new CodePropertyReferenceExpression(rentalFieldReference, "Customer"); private static CodePropertyReferenceExpression carFieldReference = new CodePropertyReferenceExpression(rentalFieldReference, "Car"); private static CodeTypeReferenceExpression carTypeReference = new CodeTypeReferenceExpression(typeof(CarType)); private static CodeTypeReferenceExpression customerTypeReference = new CodeTypeReferenceExpression(typeof(CustomerType)); private static CodeTypeReferenceExpression gasOptionReference = new CodeTypeReferenceExpression(typeof(GasOption)); private static Rule BuildRequireInsuranceRule() { Rule rule = new Rule("RequireInsurance"); rule.Priority = 15; // car type == luxury CodeBinaryOperatorExpression carTypeExp = new CodeBinaryOperatorExpression(); carTypeExp.Left = new CodePropertyReferenceExpression(rentalFieldReference, "CarType"); carTypeExp.Operator = CodeBinaryOperatorType.ValueEquality; carTypeExp.Right = new CodeFieldReferenceExpression(carTypeReference, "Luxury"); // age <= 27 CodeBinaryOperatorExpression ageExp = new CodeBinaryOperatorExpression(); ageExp.Left = new CodePropertyReferenceExpression(customerFieldReference, "Age"); ageExp.Operator = CodeBinaryOperatorType.LessThanOrEqual; ageExp.Right = new CodePrimitiveExpression(27); CodeBinaryOperatorExpression condition = new CodeBinaryOperatorExpression(); condition.Left = carTypeExp; condition.Operator = CodeBinaryOperatorType.BooleanAnd; condition.Right = ageExp; rule.Condition = new RuleExpressionCondition(condition); // create the THEN action // require insurance = true CodeAssignStatement thenAction = new CodeAssignStatement( new CodePropertyReferenceExpression(rentalFieldReference, "RequireInsurance"), new CodePrimitiveExpression(true)); rule.ThenActions.Add(new RuleStatementAction(thenAction)); return rule; } private static Rule BuildIsPremiumCustomerRule() { Rule rule = new Rule("IsPremiumCustomer"); rule.Priority = 10; // customer type ==premium CodeBinaryOperatorExpression customerTypeExp = new CodeBinaryOperatorExpression(); customerTypeExp.Left = new CodePropertyReferenceExpression(customerFieldReference, "Type"); customerTypeExp.Operator = CodeBinaryOperatorType.ValueEquality; customerTypeExp.Right = new CodeFieldReferenceExpression(customerTypeReference, "Premium"); rule.Condition = new RuleExpressionCondition(customerTypeExp); // create the THEN action // car type = luxury CodeAssignStatement thenAction = new CodeAssignStatement( new CodePropertyReferenceExpression(rentalFieldReference, "CarType"), new CodeFieldReferenceExpression(carTypeReference, "Luxury")); rule.ThenActions.Add(new RuleStatementAction(thenAction)); return rule; } private static Rule BuildGasOptionRule() { Rule rule = new Rule("GasOption"); rule.Priority = 5; // gas option == refill before return CodeBinaryOperatorExpression customerTypeExp = new CodeBinaryOperatorExpression(); customerTypeExp.Left = new CodePropertyReferenceExpression(rentalFieldReference, "GasOption"); customerTypeExp.Operator = CodeBinaryOperatorType.ValueEquality; customerTypeExp.Right = new CodeFieldReferenceExpression(gasOptionReference, "RefillBeforeReturn"); rule.Condition = new RuleExpressionCondition(customerTypeExp); // create the THEN action // required return tank level = current tank level CodeAssignStatement thenAction = new CodeAssignStatement( new CodePropertyReferenceExpression(rentalFieldReference, "MinimumTankLevelUponReturn"), new CodePropertyReferenceExpression(carFieldReference, "CurrentTankLevel")); // create the ELSE action // required return tank level = 0 CodeAssignStatement elseAction = new CodeAssignStatement( new CodePropertyReferenceExpression(rentalFieldReference, "MinimumTankLevelUponReturn"), new CodePrimitiveExpression(0)); rule.ThenActions.Add(new RuleStatementAction(thenAction)); rule.ElseActions.Add(new RuleStatementAction(elseAction)); return rule; } public static RuleDefinitions BuildRuleDefinitions() { Rule rule1 = BuildRequireInsuranceRule(); Rule rule2 = BuildIsPremiumCustomerRule(); Rule rule3 = BuildGasOptionRule(); RuleSet ruleSet = new RuleSet("CarRentalRuleSet"); ruleSet.ChainingBehavior = RuleChainingBehavior.Full; ruleSet.Rules.Add(rule1); ruleSet.Rules.Add(rule2); ruleSet.Rules.Add(rule3); RuleDefinitions ruleDefinitions = new RuleDefinitions(); ruleDefinitions.RuleSets.Add(ruleSet); WorkflowMarkupSerializer serializer = new WorkflowMarkupSerializer(); serializer.Serialize(new XmlTextWriter(@"C:\rules.xml", Encoding.Default), ruleDefinitions); return ruleDefinitions; } }
There are a couple of things going on here. First, there is logic that builds the rules using the CodeDom classes - this code isn’t specific to Windows Workflow Foundation. Here, the classes that were introduced in Table 9-4 and Table 9-5 are used to build the same rules that were built earlier using the UI. Second, there is code that takes the CodeDom expressions and creates Rule instances, and then adds these Rule instances to a RuleSet object in the BuildRuleDefinitions method. Finally, a RuleDefinitions instance is created and serialized to an XML file using WorkflowMarkupSerializer.
Given the knowledge conveyed in the last few sections, you could conceivably develop a completely customized rules editor. If end users need to be able to develop and maintain rules, the UI provided with the workflow API may or may not meet all requirements.
In reality, any interface given to users who are not developers would need to have tight controls and guidance during the rule development process. Although you may take for granted how easy it is to type this.rental.CarType, the average user’s head might explode if you expect him or her to know how to do just that, and rightfully so. Users aren’t developers, and it is not their job to be technical. It is up to you to provide an interface based on your users’ level of expertise.
A user-friendly rules editor would probably be able to determine which properties users need to make comparisons on and provide these properties in something like a drop-down box so that there is no guessing involved. Comparison operators would probably also be displayed in a list for ease of use. Whatever your rules editor ends up looking like, the flexibility of the .NET Framework and Windows Workflow Foundation allow virtually endless possibilities to meet your needs.
By default in Windows Workflow Foundation, rule definitions are associated with workflows in XML files. If you define rule conditions or rule sets in Visual Studio, the Solution Explorer window displays a <Workflow Name>.rules file associated with your workflow file (see Figure 9-7).
Figure 9-7
The following XML represents the three rental rules introduced earlier in this chapter. The nodes in the file correspond to types in the System.CodeDom namespace and are laid out in a similar fashion as the code implementation in the previous section. Some of the text has been abbreviated for spacing considerations.
<RuleDefinitions xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow"> <RuleDefinitions.RuleSets> <RuleSet Name="RentalRules" ChainingBehavior="Full" Description="{p3:Null}" xmlns:p3="http://schemas.microsoft.com/winfx/2006/xaml"> <RuleSet.Rules> <Rule Name="Require Insurance" ReevaluationBehavior="Always" Priority="15" Description="{p3:Null}" Active="True"> ... </Rule> <Rule Name="Is Premium Customer" ReevaluationBehavior="Always" Priority="10" Description="{p3:Null}" Active="True"> <Rule.ThenActions> <RuleStatementAction> <RuleStatementAction.CodeDomStatement> <ns0:CodeAssignStatement LinePragma="{p3:Null}" xmlns:ns0="clr-namespace:System.CodeDom;Assembly=System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"> <ns0:CodeAssignStatement.Left> <ns0:CodePropertyReferenceExpression PropertyName="CarType"> <ns0:CodePropertyReferenceExpression.TargetObject> <ns0:CodeFieldReferenceExpression FieldName="rental"> <ns0:CodeFieldReferenceExpression.TargetObject> <ns0:CodeThisReferenceExpression /> </ns0:CodeFieldReferenceExpression.TargetObject> </ns0:CodeFieldReferenceExpression> </ns0:CodePropertyReferenceExpression.TargetObject> </ns0:CodePropertyReferenceExpression> </ns0:CodeAssignStatement.Left> <ns0:CodeAssignStatement.Right> <ns0:CodeFieldReferenceExpression FieldName="Luxury"> <ns0:CodeFieldReferenceExpression.TargetObject> <ns0:CodeTypeReferenceExpression> <ns0:CodeTypeReferenceExpression.Type> <ns0:CodeTypeReference ArrayElementType="{p3:Null}" BaseType="RulesTesting.CarType" Options="0" ArrayRank="0" /> </ns0:CodeTypeReferenceExpression.Type> </ns0:CodeTypeReferenceExpression> </ns0:CodeFieldReferenceExpression.TargetObject> </ns0:CodeFieldReferenceExpression> </ns0:CodeAssignStatement.Right> </ns0:CodeAssignStatement> </RuleStatementAction.CodeDomStatement> </RuleStatementAction> </Rule.ThenActions> <Rule.Condition> <RuleExpressionCondition Name="{p3:Null}"> <RuleExpressionCondition.Expression> <ns0:CodeBinaryOperatorExpression Operator="ValueEquality" xmlns:ns0="..."> <ns0:CodeBinaryOperatorExpression.Left> <ns0:CodePropertyReferenceExpression PropertyName="Type"> <ns0:CodePropertyReferenceExpression.TargetObject> <ns0:CodePropertyReferenceExpression PropertyName="Customer"> <ns0:CodePropertyReferenceExpression.TargetObject> <ns0:CodeFieldReferenceExpression FieldName="rental"> <ns0:CodeFieldReferenceExpression.TargetObject> <ns0:CodeThisReferenceExpression /> </ns0:CodeFieldReferenceExpression.TargetObject> </ns0:CodeFieldReferenceExpression> </ns0:CodePropertyReferenceExpression.TargetObject> </ns0:CodePropertyReferenceExpression> </ns0:CodePropertyReferenceExpression.TargetObject> </ns0:CodePropertyReferenceExpression> </ns0:CodeBinaryOperatorExpression.Left> <ns0:CodeBinaryOperatorExpression.Right> <ns0:CodeFieldReferenceExpression FieldName="Premium"> <ns0:CodeFieldReferenceExpression.TargetObject> <ns0:CodeTypeReferenceExpression> <ns0:CodeTypeReferenceExpression.Type> <ns0:CodeTypeReference ArrayElementType="{p3:Null}" BaseType="RulesTesting.CustomerType" Options="0" ArrayRank="0" /> </ns0:CodeTypeReferenceExpression.Type> </ns0:CodeTypeReferenceExpression> </ns0:CodeFieldReferenceExpression.TargetObject> </ns0:CodeFieldReferenceExpression> </ns0:CodeBinaryOperatorExpression.Right> </ns0:CodeBinaryOperatorExpression> </RuleExpressionCondition.Expression> </RuleExpressionCondition> </Rule.Condition> </Rule> <Rule Name="Gas Option" ReevaluationBehavior="Always" Priority="5" Description="{p3:Null}" Active="True"> ... </Rule> </RuleSet.Rules> </RuleSet> </RuleDefinitions.RuleSets> </RuleDefinitions>