in

Foo Theory

Partners in Community - serving up some ice cold Kool-Aid!
Welcome to footheory.com.  The bloggers and contributing members on this site are consultants, project/program managers and software architects working across the US.  Our community will focus on Microsoft technologies, .NET architecture, software patterns & practices and just plain stream of consciousness.

Bennie's Weblog

May 2007 - Posts

  • Sample SMO Activities

    I uploaded a WF custom activity library, which contains two activities:

    • BackupDatabase: Performs a full or incremental backup of a database
    • RestoreDatabase: Performs a restore of a database

    Both activities leverage the SQL Server 2005 System Management Objects (SMO). For details on the file contents, please refer to our downloads folder.

    This sample illustrates a couple of interesting concepts, specifically:

    1. Implementing a validator for a custom activity.  
    2. Developing a custom Designer for an activity.
    3. Leveraging inheritance to develop custom activities.
    4. Leveraging a custom WorkflowRuntimeService to provide feedback about long-running operations to the hosting application.

    I am planning on writing a number of follow-up posts, which will highlight some of the more interesting aspects of these activities, so stay tuned!

  • Simple Application Extensibility with WF Rules

    Overview

    In a previous article, we studied how we can externalize Windows Workflow Rules into a separate database, allowing the rules to be modified without a need to rebuild or re-deploy the application.

    In this post, we take a lot at how we can use WF Rules to implement simple application extensibility. For some applications, you might want to enhance the flexibility and extensibility of your application without having to invest extraordinary amounts of time in devising and testing a complicated plug-in architecture.  This article will show how you can leverage WF Rules using the RuleSetService together with a minimum amount of custom code to accomplish this goal.

    Applying Rules to non-Workflow types

    Typically, Windows Workflow Foundation Rules are applied to Workflow Type, either as a Declarative Rule Condition, or as part of a PolicyActivity. Within a Rule condition and/or its associate "If" or "Else" Actions  you can access the Workflow fields, properties and methods. In addition, the Rule can also access the members of objects within your Workflow class.

    The Rules sub-system of Windows Workflow Foundation was architected to be independent the Workflow types, and RuleSets were designed to be able to operate on ANY type, not just a type derived from System.Workflow.ComponentModel.Activity. Therefore, you can apply a RuleSet to any of the types in your own assemblies.

    This fact opens up some interesting possibilities. In a future post, I will present a sample that uses WF rules to perform Rule-based custom validation of WinForms and ASP.NET Web forms. In this post, we will focus on how to leverage WF Rules to achieve simple application extensibility.

    An overview of how such a concept can be implemented is shown in the following figure:

    Extensibility

    A description of the most important interactions between the different components is provided below:

    1. The application defines an extensibility point. This extensibility point contains code which leverages the RuleSetService to load, validate and execute a RuleSet.
    2. The RuleSetService loads the RuleSet from the Rules database.
    3. The RuleSet has been written using the RuleSetTool.exe Rule editor.  The .NET type it operates on is an Extensibility Interface, which is implemented by the application. When the developer created the RuleSet, he selected the type of this extensibility interface as the "Associated Type" for the RuleSet. For details on how to do this, refer to the "Creating the RuleSet" section of this post
    4. The different Rules that make up the RuleSet are applied during the execution of the RuleSet. During their execution, these Rules can invoke the properties and methods defined by the extensibility interface.
    5. Because the application implements the extensibility interface, the RuleSet is able to extend the functionality of the application, implement custom behavior etc.

    In the above pattern, note that the use of an Extensibility Interface is not absolutely necessary. But, if we provided the main type of the application itself (for example the Main Form class) as the associated type of the workflow, then our Rules would be able to affect all aspects of our application, which is probably NOT what we really want. Using an extensibility interface allows the designer to reduce the "surface area" of the application on which the RuleSet can operate.

    Sample Application

    In this sample we assume that we are working on an e-commerce application. We will create a simple WinForms application, which displays a grid  of summary records of Return Merchandise Authorization (RMA) information, grouped by Product Code. Each summary record contains the following information:

    • The Product Code
    • The number of returns and their monetary value for the current quarter.
    • The number of returns and their monetary value for the previous quarter.
    • The number of returns and their monetary value for the current year (year-to-date).

    Our application should be able to tailor the columns shown based upon the currently logged-in user. An application might  want to do this for both security- and job functionality reasons (people in certain functional areas might want to see only the information that is relevant to them).

    In our sample application, we will include a drop-down where we can select the user for which we want to test the application. In a real-world application, you would probably use the WindowsIdentity class to determine the current user's identity.

    Creating the application skeleton

    Create a new WinForms application, and add the following references:

    1. System.Workflow.Activities.dll
    2. System.Workflow.ComponentModel.dll
    3. System.Workflow.Runtime.dll
    4. RuleSetServices.dll: This is our RuleSetService implementation mentioned in the "external rules" article.
    5. ExternalRuleSetLibrary.dll: This is the Entity library for the RuleSetService.

    We will model the RMA information by a class called RMAQuartelySummary.cs. Add this class to the project. This is a simple data container class, the source code is shown below:

    1 using System; 2 3 namespace SampleApplication 4 { 5 /// <summary> 6 /// These are the quarterly summary figures. 7 /// Each instance represents the figures for 8 /// one particular product 9 /// </summary> 10 public class RMAQuarterlySummary 11 { 12 private String m_productCode; 13 private int m_numberOfReturnsThisQuarter; 14 private int m_numberOfReturnsLastQuarter; 15 private int m_ytdNumberOfReturns; 16 private Decimal m_valueOfReturnsThisQuarter; 17 private Decimal m_valueOfReturnsLastQuarter; 18 private Decimal m_ytdValueOfReturns; 19 20 public String ProductCode 21 { 22 get { return m_productCode; } 23 set { m_productCode = value; } 24 } 25 26 public int NumberOfReturnsThisQuarter 27 { 28 get { return m_numberOfReturnsThisQuarter; } 29 set { m_numberOfReturnsThisQuarter = value; } 30 } 31 32 public int NumberOfReturnsLastQuarter 33 { 34 get { return m_numberOfReturnsLastQuarter; } 35 set { m_numberOfReturnsLastQuarter = value; } 36 } 37 38 public Decimal ValueOfReturnsThisQuarter 39 { 40 get { return m_valueOfReturnsThisQuarter; } 41 set { m_valueOfReturnsThisQuarter = value; } 42 } 43 44 public Decimal ValueOfReturnsLastQuarter 45 { 46 get { return m_valueOfReturnsLastQuarter; } 47 set { m_valueOfReturnsLastQuarter = value; } 48 } 49 50 public int YtdNumberOfReturns 51 { 52 get { return m_ytdNumberOfReturns; } 53 set { m_ytdNumberOfReturns = value; } 54 } 55 56 public Decimal YtdValueOfReturns 57 { 58 get { return m_ytdValueOfReturns; } 59 set { m_ytdValueOfReturns = value; } 60 } 61 62 public RMAQuarterlySummary( 63 string productCode, 64 int numberOfReturnsThisQuarter, 65 Decimal valueOfReturnsThisQuarter, 66 int numberOfReturnsLastQuarter, 67 Decimal valueOfReturnsLastQuarter, 68 int ytdNumberOfReturns, 69 Decimal ytdValueOfReturns) 70 { 71 m_productCode = productCode; 72 m_numberOfReturnsThisQuarter = numberOfReturnsThisQuarter; 73 m_valueOfReturnsThisQuarter = valueOfReturnsThisQuarter; 74 m_numberOfReturnsLastQuarter = numberOfReturnsLastQuarter; 75 m_valueOfReturnsLastQuarter = valueOfReturnsLastQuarter; 76 m_ytdNumberOfReturns = ytdNumberOfReturns; 77 m_ytdValueOfReturns = ytdValueOfReturns; 78 } 79 } 80 } 81

    Our grid will always display the following columns:

    1. The Product Code (property ProductCode).
    2. The Number of Returns for the current quarter (property NumberOfReturnsThisQuarter).

    Our RuleSet will determine which additional columns will be shown, based on a set of properties of the currently logged-in user. We will use the RMACustomColumnType enumeration to represent these custom columns. This enumeration is shown below:

    1 using System; 2 3 namespace SampleApplication 4 { 5 /// <summary> 6 /// These are the different types of columns 7 /// that can optionally be displayed in 8 /// our Grid 9 /// </summary> 10 public enum RMACustomColumnType 11 { 12 ValueOfReturnsThisQuarter, 13 NumberOfReturnsLastQuarter, 14 ValueOfReturnsLastQuarter, 15 YtdNumberOfReturns, 16 YtdValueOfReturns 17 } 18 } 19

    We also need a class to represent a user. In a real-world scenario, we would probably use Activity Directory or some other LDAP provider. To keep it simpe, we will use a class called BusinessUser.cs, as shown below:

    1 using System; 2 3 namespace SampleApplication 4 { 5 /// <summary> 6 /// This class represents our users 7 /// </summary> 8 public class BusinessUser 9 { 10 private string m_name; 11 private bool m_isAccountant; 12 private bool m_isManager; 13 14 public string Name 15 { 16 get { return m_name; } 17 } 18 19 public bool IsAccountant 20 { 21 get { return m_isAccountant; } 22 } 23 24 public bool IsManager 25 { 26 get { return m_isManager; } 27 } 28 29 public BusinessUser(string name, bool isAccountant, bool isManager) 30 { 31 m_name = name; 32 m_isAccountant = isAccountant; 33 m_isManager = isManager; 34 } 35 } 36 } 37

    Note the two properties:

    1. bool IsAccountant: This property indicates that the user is an accountant.
    2. bool IsManager: This property indicates that the user is a manager.

    These properties will indirectly be used by our Ruleset to determine which custom columns should be shown in the grid.

    Defining the Extensibility Interface

    Next, we will define the extensibility interface that will be exposed by our application to the WF Rules. Below is a summary of the requirements for this interface:

    1. The interface should have access to the IsAccountant and IsManager boolean properties of the current user.
    2. The interface should enable the Rules to add a custom column and a custom header text to the grid view.

    Add a new interface to the project, and name it IGridExtensibility.cs.The code for this class is shown below:

    1 using System; 2 3 namespace SampleApplication 4 { 5 /// <summary> 6 /// Thi is our extensibility interface 7 /// </summary> 8 public interface IGridExtensibility 9 { 10 bool IsCurrentUserAccountant { get; } 11 bool IsCurrentUserManager { get; } 12 13 void AddColumn( 14 RMACustomColumnType columnType, 15 string headerText); 16 } 17 } 18

    Implementing the Application's Main Form

    The design of the main form is shown below. We use a DataGridView to show the data, and a drop-down combo to allow for the selection of the current user:

    FormDesign

    The complete code for the main form is available in the downloads section, I will focus on the most important code sections below.

    • Our Form will implement the IGridExtensibility interface, as shown below:

    1 namespace SampleApplication 2 { 3 public partial class MainForm : Form, IGridExtensibility 4 {

    • To simulate a number of users, we will create a List of BusinessUsers as a private member. Our drop-down combo will allow us to select a user:

    1 private List<BusinessUser> m_users = new List<BusinessUser>();

    1 // Create three different users 2 m_users.Add(new BusinessUser("Joe User", false, false)); 3 m_users.Add(new BusinessUser("Fred Beancounter", true, false)); 4 m_users.Add(new BusinessUser("Jack Sledgehammer", false, true));

    Note that the first user ("Joe User") is neither an accountant nor a manager, while "Fred Beancounter" is an accountant. The final user "Jack Sledgehammer" is a manager (what else did you expect?).

    • The implementation of this interface is shown below:

    1 #region IGridExtensibility Members 2 // This property is used by our Rules to determine if 3 // the current user is an accountant 4 public bool IsCurrentUserAccountant 5 { 6 get { return m_currentUser.IsAccountant; } 7 } 8 9 // This property is used by our Rules to determine if 10 // the current user is a Manager 11 public bool IsCurrentUserManager 12 { 13 get { return m_currentUser.IsManager; } 14 } 15 16 // This method is used by our Rules to add a Custom Column 17 public void AddColumn(RMACustomColumnType columnType, string headerText) 18 { 19 DataGridViewTextBoxColumn column = new DataGridViewTextBoxColumn(); 20 column.HeaderText = headerText; 21 column.DataPropertyName = columnType.ToString(); 22 column.Name = columnType.ToString(); 23 24 m_userColumns.Add(column); 25 } 26 27 #endregion IGridExtensibility Members

    In lines 4 through 14, we implement the properties that will be used by the RuleSet to access the properties of the current user. The application maintains the current user in the m_currentUser private field.

    The AddColumn method is the method that will be called by the Rule to add a custom column to the grid. The code simply creates a new DataGridViewTextBoxColumn instance, and adds it to the m_userColumns list. This list will then be used by the application to create the custom columns in the grid (see next section).

    The showColumns method is the method that contains our extensibility point. In this method we load, validate and execute our RuleSet:

    1 // This method shows both the standard and custom columns 2 // for our grid 3 private void showColumns() 4 { 5 // First, show the standard columns 6 showStandardColumns(); 7 m_userColumns.Clear(); 8 9 // Use the RuleSetService to retrieve our RuleSet 10 RuleSetService ruleSvc = new RuleSetService(); 11 RuleSet ruleSet = 12 ruleSvc.GetRuleSet(new RuleSetInfo("RmaRules")); 13 14 // Setup a RuleValidation instance 15 RuleValidation validation = new RuleValidation(this.GetType(), null); 16 17 // Validate the RuleSet 18 if (ruleSet.Validate(validation)) 19 { 20 RuleExecution execution = new RuleExecution(validation, this); 21 ruleSet.Execute(execution); 22 23 // Show each custom column added by the rule 24 foreach (DataGridViewColumn column in m_userColumns) 25 { 26 RMAGrid.Columns.Add(column); 27 } 28 } 29 }

    In lines 10-12, we instantiate the RuleSetService, and load our "RmaRules" RuleSet from the Rules database.

    Before we can execute our RuleSet, we need to Validate it. We do this by creating a RuleValidation instance, passing in our type, which in this case is our extensibility interface type. Optionally, you can pass in a custom ITypeProvider interface as the second argument, which can further restrict which types are available to the RuleSet.

    In line 15 we call the Validate method on the RuleSet, passing in our RuleValidation instance.  The Validate method will ensure that all Rules in our RuleSet are accessing valid fields, properties and methods of the type passed in as the first argument to the RuleValidation constructor.

    To execute the RuleSet, we first need to instantiate  a RuleExecution object. The constructor takes the following arguments:

    • Our previously instantiated RuleValidation instance. Passing in a RuleValidation instance guarantees that its contained rules are defined correctly.
    •  Our Form instance, which is the object on which the RuleSet will operate. All access to this object will be performed through the extensibility interface, since this is the type that was specified when we create the RuleSet in the RuleEditor (see next section).

    Finally, we can execute the RuleSet by invoking its Execute method, passing in our RuleExecution instance. This will trigger our Rules to fire, invoking the methods in our extensibility interface. Note that this execution will be performed on our current thread, so we have no need for thread synchronization primitives here.

    After our Rule execution is complete, we simply add the custom columns to our Grid (lines 24-26).

    Creating the RuleSet

     To create the RuleSet, execute the RuleSet Editor RuleSetTool.exe (found in the ExternalRuleSetSample dialog). Create a new RuleSet named RmaRules with version 1.0. Associate the RuleSet with the SampleApplication.IGridExtensibility interface, as shown below:

     CreateRule

    we will create two rules:

    • ColumnsForBeanCounter: The condition of this rule checks if the current user's IsAccountant flag is set:

    this.IsCurrentUserAccountant

     The "then action" adds the following custom columns to the grid:

      • ValueOfReturnsThisQuarter
      • ValueOfReturnsLastQuarter
      • YtdValueOfReturns

    this.AddColumn(SampleApplication.RMACustomColumnType.ValueOfReturnsThisQuarter, "Money Lost this Quarter") this.AddColumn(SampleApplication.RMACustomColumnType.ValueOfReturnsLastQuarter, "Money Lost last Quarter") this.AddColumn(SampleApplication.RMACustomColumnType.YtdValueOfReturns, "Money Lost last Year")

    • ColumnsForManager: The condition of this rule check if the current user's IsManager flag is set:

    this.IsCurrentUserManager

    The "then action" adds the following custom columns to the grid:

    • YtdTotalNumberOfReturns
    • YtdValueOfReturns
  • this.AddColumn(SampleApplication.RMACustomColumnType.YtdNumberOfReturns, "Total Returns this Year") this.AddColumn(SampleApplication.RMACustomColumnType.YtdValueOfReturns, "Values of Returns for this year")

    The Rules editor with the completed RuleSet is shown below:

    ExtensibilityRulesFinished

    Running the Application

    When you run the application, and you select the first user ("Joe User"), you will notice that only the standard columns are displayed:

    JoeUser

    When you select "Fred Beancounter", you will see the "accountant" custom columns:

    FredBeancounter

    An finally, our friend Sledgehammer will see the manager columns:

    JackSledgeHammer

    Conclusion

    In this example, you have seen that the power of WF RuleSets can be leveraged outside of the realm of Workflow. Because a RuleSet can be associated and operate on any .NET type, it can be applied to any number of application domains. In the next post, we will look at another interesting application of WF Rules: providing complex validation of WinForms and Web forms.

     

  • Alternative Applications for the WF Rule Engine

    A Quick Overview of WF Rules

    Windows Workflow Foundation (WF) includes a flexible Rule Engine, allowing the Workflow developer to logically separate your application's business rules from the Workflow Assemblies. Rules allow you to model and implement the business requirements of your application into a set of declarative statements, which can be enforced at runtime.

    A WF Rule is composed out of the following three separate parts:

    1. A Condition which evaluates to a Boolean result.
    2. A Then Action which is executed if the condition evaluates to true.
    3. An optional Else Action which is executed if the condition evaluates to false.

    The WF Visual Studio designers include a set of Rule Editor Dialogs. These editors allows the developer to create individual rules, and combine them together into a RuleSet. Alternatively, you can use any .NET language to create the a RuleSet through custom code.

    Traditionally, WF Rules are used in the following two context:

    1. Within a Declarative Rule Condition. Such a rule condition can be part of an IfElseBranchActivity, a WhileActivity, or any other activity which needs to evaluate a simple or compound boolean expression.
    2. Within a Policy activity. A Policy activity evaluates a RuleSet, and applies the Then or Else actions of each Rule in the RuleSet, based upon the Rule's condition. The Rule Engine has the ability to apply forward chaining, which is a mechanism in which dependencies within rules are automatically detected, and Rules can be re-evaluated as a result of these dependencies.

    In this article, we will go beyond the traditional approach outlined above, and look at some exciting things we can do with WF Rules. Specifically, we look at the following:

    1. First, we take a look at how can externalize a rule set, thereby decoupling it from the Workflow Assemblies in which they are applied.
    2. Next, we will take a look at how we can apply an external RuleSet to perform complex validation on WinForms and ASP.NET Web Forms.
    3. Finally, we will illustrate a mechanism through which you can achieve application extensibility by means of a WF RuleSet and targeted application extension points.

    Externalizing WF Rules

    Why externalize Rules?

    Within a typical WF application, the Rules defined within the scope of a Workflow are serialized to a .rules file that is compiled as an embedded resource to your Workflow assembly. The .rules resource is deserialized and evaluated at runtime. While this approach might be completely feasible for some applications, it does require that the application is re-built and re-deployed whenever any of the rules change.

    The External RuleSet Sample Project

    At the WF site on .NetFx3.com, there is a sample available called "External RuleSet Demo", which illustrates how you can store WF RuleSets in an external database. Leveraging a database allows us to manage, edit and deploy the RuleSets without the need to rebuild and re-deploy our Workflow Assemblies.

    This sample includes a setup script, which creates a database called "Rules". The schema for this database is very simple, as is illustrated below:

     RulesDatabase

    The Rules database has a single table called RuleSet. Each record in the RuleSet represents a particular version of a RuleSet (identified by Name, Major- and Minor version).

    The RuleSet itself is serialized as XML into the RuleSet column. The Status column indicates whether or not the RuleSet has been validated successfully.

    The Visual Studio solution of the sample contains the following projects:

    1. ExternalRuleSetLibrary. This project contains the definition of the RuleSetData and RuleSetInfo classes, which model a RuleSet representation in the Rules database.
    2. RuleSetService. This service implements a WorkflowRumetime service, which allows you to load a specified RuleSet from the Rules database.
    3. PolicyActivities. This project is a custom activity library, which implements a custom Policy activity called RunMembershipRules which leverages the RuleSetService to load the specified RuleSet from the Rules database, instead of loading it from an embedded .rules resource as is done by the standard PolicyActivity.
    4. RuleSetTool. This is a windows application, which allows you to:
      1. Load and save RuleSets from the Rules Database.
      2. Create a new RuleSet
      3. Import a RuleSet from a .rules file into the Rules database, or export a RuleSet from the Rules database to a .rules file.
      4. Edit a RuleSet, and associate it with an external type  which is typically (but not necessarily) a Workflow. We will have more to say about this later on in this article. This editor is basically the standard Rules editor that you use when you edit a RuleSet associated with a standard Policy activity in Visual Studio.

    Using an external RuleSet in your Workflow Project

    To create a Workflow which leverages a RuleSet defined in the Rules database is very simple:

    • First, you need to create the database by running the setup.cmd script in the root directory. Make sure to modify the invoked setup.sql script to target the appropriate SQL Server instance.
    • Next, open the ExternalRuleSetToolkit solution and build the project.
    Sample Project Description

    Once you have this preparation work out of the way, you can start using the RuleSetService and custom Policy Activity in your own WF projects. In this article, we will create a simple workflow with the following requirements:

    Our workflow is part of an e-commerce system, which allows it's customers to sign up as members. When a customer signs up, he/she has the choice between 3 levels of memberships:

    1. Standard. This level of membership allows a member to purchase products at retail cost. This membership is free.
    2. Gold. Gold members get 10% discount on all products. The membership fee for this level is $75/year.
    3. Platinum. Platinum members get 15% off on all products. Also, if a platinum member places an order with a final cost (after discount) of more than $200, then the company will waive their standard shipping cost of $5. The membership fee for this level is $100/year, except when the final order cost is greater than $300, in which case the membership cost for the first year is only $90.
    Workflow Design

    Our workflow is invoked when the member signs up, and (optionally) places his first order. The workflow will be invoked with the following parameters:

    • The Membership Level (represented by a member of the MemberShipLevel enumeration).
    • The total cost of the purchased products.

    Upon completion, the workflow will make the following output parameters available:

    • The membership cost
    • The final cost of the member's initial order.
    • The shipping cost.
    Creating the Workflow Project

    To implement this workflow, follow these steps:

    • Create an empty workflow project called NewMemberSignup, and delete the default workflow (Workflow1.cs) from the project.
    • Add a reference to the PolicyActivities.dll and the RuleSetServices.dll projects.
    • Add a new Sequential workflow (with Code Separation) to the project, and call it NewMemberSignup.xoml, as is shown below:

    CreateWorkflow

    (Personally, I always prefer XAML-based workflows with code separation above "pure code" workflows, since I can modify them with both Visual Studio AND my favorite XML editor ;-).

    • Add the MembershipLevel enumeration to the project:

    /// <summary> /// These are our different Membership Levels /// </summary> public enum MembershipLevel { Standard, Gold, Platinum }

    • Switch to the code-behind of the Workflow, and add a private field and a public property for each workflow parameter. The code is shown below:

      public partial class NewMemberSignup : SequentialWorkflowActivity { // Input parameters private MembershipLevel m_membershipLevel; public MembershipLevel MembershipLevel { get { return m_membershipLevel; } set { m_membershipLevel = value; } } private Decimal m_ordercost; public Decimal OrderCost { get { return m_ordercost; } set { m_ordercost = value; } } // Output parameters private Decimal m_membershipCost; public Decimal MembershipCost { get { return m_membershipCost; } set { m_membershipCost = value; } } private Decimal m_finalOrderCost; public Decimal FinalOrderCost { get { return m_finalOrderCost; } set { m_finalOrderCost = value; } } private Decimal m_shippingCost; public Decimal ShippingCost { get { return m_shippingCost; } set { m_shippingCost = value; } } }
    • Add the PolicyFromService activity to the toolbox (by right-clicking a toolbox tab area, and selecting "choose Items", and browsing to the PolicyActivities.dll assembly):

    Toolbox

    • Drag the PolicyFromServiceActivity in the Workflow, between the start and end markers. Rename the activity RunMembershipRules. This activity will be the only activity in the workflow.
    • Build the project. It is important the build the project BEFORE you create the RuleSet, otherwise your Workflow Type will not have the properties you just added available for use in the RuleSet editor.
    • Before we can set the RuleSet name in this activity, we should first go ahead and create out RuleSet. Start the RuleSetTool from the demo project, and create a new Rule by clicking the New button. Name the RuleSet NewMemberSignup, and keep the version # at "1.0".
    • Now, we need to associate the RuleSet with our Workflow, so it can access our properties. Use the Browse button to navigate to the bin\debug directory and select the NewMemberSignup.dll assembly, and the NewMemberSignup.NewMemberSignup type, as shown below:

    SelectType

    • At this time, it is best to save the created RuleSet by selection RuleStore | Save.
    • Next, click Edit Rules to start creating the RuleSet contents. Click Add Rule to add the first rule. Name this rule CalcGoldMembershipCost. The details of the rule are shown below:

    CalcGoldMembershipCost

    • Add a rule named CalcPlatinumMembershipCost to calculate the cost for a platinum member:

    CalcPlatinumMembershipCost

    Following are the CalcDiscountGoldMembership and CalcDiscountPlatinumMembership rules:

    CalcDiscountGoldMembership

     

    CalcDiscountPlatinumMembership

    we also need a rule to set the final order cost to the input order cost in case the member is a standard member (rule name: CalcNoDiscountStandardMember):

    CalcNoDiscountStandardMember

    and finally, we need a rule to adjust the Membership fee for a Platinum member with a final order amount about $300 (rule name: AdjustPlatinumMembershipCost):

    AdjustPlatinumMembershipCost

    • Now that we have our RuleSet created, we need to reference it correctly in the Workflow editor. Switch back to the design view for the workflow, and Set the RuleSet name, major and minor version as shown below:

    CustomActivityProperties

    Creating a test project

    Now that our workflow is complete, we need a little test program to test our Workflow as follows:

    • Add Sequential Workflow Console Application to your solution and name it TestNewMemberSignup. You can delete the Workflow1 workflow created for the project, since we will be using our Workflow in the NewMemberSignup project. Make sure to add the following references to your console application: 
      • A project reference to this project in your new console application.
      • A reference to the RuleSetService
    • We will be retrieving the RuleSet from the Rules database. Therefore, we will need an application configuration file with the appropriate connection string, as shown below:

    <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="RuleSetStoreConnectionString" connectionString="Initial Catalog=Rules;Data Source=localhost;Integrated Security=SSPI;" providerName="System.Data.SqlClient"/> </connectionStrings> </configuration>

    • As we start implementing the code for our test project, let's summarize what this code should do:
      • The program will received test values for the Membership Level and the Order Cost as command-line arguments
      • We should  create an instance of the workflow runtime.
      • The RuleSetService is implemented as a standard workflow runtime service (it inherits from System.Workflow.Runtime.Hosting. WorkflowRuntimeService), therefore we can add it directly to the Workflow runtime.
      • We should pass the MembershipLevel and the OrderCost as arguments to the workflow, and start the workflow.
      • When the workflow terminates, we should retrieve the output arguments.
    • The full code is shown below (note: You can download all of the source code in the downloads section of our site):

    1 using System; 2 using System.Collections.Generic; 3 using System.Threading; 4 using System.Workflow.Runtime; 5 using System.Workflow.Runtime.Hosting; 6 7 using RuleSetServices; 8 using NewMemberSignup; 9 10 namespace TestNewMemberSignup 11 { 12 class Program 13 { 14 private static AutoResetEvent waitHandle; 15 16 static void Main(string[] args) 17 { 18 if (args.Length != 2) 19 { 20 Usage(); 21 return; 22 } 23 24 // Create the workflow runtime 25 using(WorkflowRuntime workflowRuntime = new WorkflowRuntime()) 26 { 27 // Create our wait event, hookup our handlers 28 waitHandle = new AutoResetEvent(false); 29 workflowRuntime.WorkflowCompleted += workflowRuntime_WorkflowCompleted; 30 workflowRuntime.WorkflowTerminated += workflowRuntime_WorkflowTerminated; 31 32 // Add the RuleSetService 33 workflowRuntime.AddService(new RuleSetService()); 34 35 // Create our arguments 36 Dictionary<string, object> wfArguments = 37 new Dictionary<string, object>(); 38 39 MembershipLevel membershipLevel = 40 (MembershipLevel)Enum.Parse(typeof(MembershipLevel), args[0]); 41 Console.WriteLine("Membership Level: {0}", membershipLevel); 42 wfArguments.Add("MembershipLevel", membershipLevel); 43 44 decimal orderCost = decimal.Parse(args[1]); 45 Console.WriteLine("Order Cost: {0}", orderCost); 46 wfArguments.Add("OrderCost", orderCost); 47 48 // Start the Workflow 49 WorkflowInstance instance = workflowRuntime.CreateWorkflow( 50 typeof(NewMemberSignup.NewMemberSignup), wfArguments); 51 instance.Start(); 52 53 // Wait for the Workflow to complete 54 waitHandle.WaitOne(); 55 } 56 } 57 58 static void workflowRuntime_WorkflowCompleted( 59 object sender, 60 WorkflowCompletedEventArgs e) 61 { 62 Console.WriteLine("Membership Cost: {0}", 63 e.OutputParameters["MembershipCost"]); 64 Console.WriteLine("Final order price: {0}", 65 e.OutputParameters["FinalOrderCost"]); 66 Console.WriteLine("Shipping Cost: {0}", 67 e.OutputParameters["ShippingCost"]); 68 69 waitHandle.Set(); 70 } 71 72 static void workflowRuntime_WorkflowTerminated( 73 object sender, 74 WorkflowTerminatedEventArgs e) 75 { 76 Console.WriteLine("Workflow terminated, reason: {0}", e.Exception.Message); 77 waitHandle.Set(); 78 } 79 80 private static void Usage() 81 { 82 Console.WriteLine("Usage <MembershipLevel> <OrderCost>"); 83 } 84 } 85 } 86

    The most important aspects of the above code are discussed below:

    • At lines 28 through 30, we hookup the WorkflowCompleted and WorkflowTerminated events. It is a good habit to ALWAYS process  the WorkflowTerminated event, this way you will immediately know if something went wrong with your workflow. We need to process WorkflowCompleted, since this is the only way that we can access the OUTPUT arguments of the workflow. The OutputParameters property of the WorkflowCompletedEventArgs class contains a Dictionary with the output parameters. We display those in lines 62-67.
    • We add the RuleSetService to the WorkflowRuntime in line #33.
    • To pass input arguments to a workflow, we need to create a Dictionary<String, Object> instance, and add our values. Note that our keys should be identical to the public property names used in our Workflow (lines 36-42). The CreateWorkflow method of the WorkflowRuntime takes the Workflow arguments as the second parameter (lines 49-50).
    Running the Test Project

     Below is a table with the results of a number of test runs:

    TestResults

    From the above table, it is clear that our rules were evaluated as expected. For tests 2 and 3, the discounts and membership fees are calculated as expected. For test number four, we see that because the discounted order cost is above 200, the member gets free shipping.

    The most interesting result is the last result. Because the OrderCost is greater than $300, the membership fee is reduced from $100 to $90. To avoid a result where Rule # 3 (calculation of Platinum membership cost) would override Rule # 5 (discount for platinum members with a total order > $300), we need to make sure that we know how rules are evaluated.

    Within the same priority, rules are evaluated in alphabetical order. Since Rule #5 starts with an "a", it will be evaluated first, so Rule #3 will be evaluated later. This would undo the result of Rule #5. Therefore, we will give Rule#3 a HIGHER priority then Rule # 5,so that it will be evaluated before Rule #. As you can see, this provides us with the expected result.

    What is so powerful of external rules is that I could now go in and change the above rules with the RuleSet editor, and re-run the application without any recompilation, and I would immediately get result that would reflect these adjusted rules!

    Performing Forms Validation with WF Rules

     Because of the length of this post, we will defer this topic to a later post.

    WF Rules and Application Extensibility

    Same here.. Stay tuned!