Tuesday, November 15, 2011

Using WF4 as a Business Rule Engine

Introduction

A project at work forced me to get up to speed on Windows Workflow Foundation (WF) quickly. My boss wanted to use it as a business rule engine where the end user could edit the rules themselves. They didn't necessarily need full flexibility on the editing, but the core parameters of the rules had to be editable. Now there's probably hundred ways to approach this, and many ways to do it without using Workflow Foundation, but I decided do a proof of concept doing so.

I then came over Christophe Geers' Blog and the article Using Rules outside of a Workflow. This was the kind of kick-start I needed - or so I thought, until I noticed it targets WF3 (Workflow for .NET 3.5). Now I could go on and use WF3, but some Googling and reading lead me to believe i would be better prepared for the future using WF4 instead*. In this article I will implement the same application as Christophe Geers did, but in Workflow 4.

*) You may argue against this in the comment field if you like - I'm still not sure this is an obvious choice.

Workflow 3 vs 4

So what has changed? A lot. Microsoft has rewritten the Workflow framework quite extensively from WF3 to WF4, and the way you work with Rules is fundamentally changed. Long story short, I feel they've tried to generalize the concept of an element in a workflow to the point where "everything is an apple". Maybe they've succeeded - I really haven't worked long enough with the framework to comment on this. Anyway, a Rule is now a basically an Activity configured accordingly. The Rule class is obsolete. I will not go into detail on the changes - Microsoft have published a 17 page document comparing how to use the concept of Rules in the document WF Guidance: Rules which is a part of the WF4 Migration guide

The Salutation Resolver Solution

Christophe Geers implemented a rule set in WF3 which basically decides the correct salutation in a letter for a person given some basic properties about that person. I'm not going to go into the same level of detail regarding the problem domain as Geers did, instead I'll focus on how to implement it in WF4. For reference, go to his blog entry at any time. This table will describe the expected result though:






GenderAgeMarriedSalutation
M>18Yes/NoMr
F>18NoMiss
F>18YesMrs
F/M<18Yes/NoTo the parents of

The solution will be divided into three projects:

  1. A shared library with the business entities
  2. A Forms project which take input and allow you to edit rules
  3. A Workflow Activity project. This is where the juice is.

The Forms project contains two forms. The parameter input form looks like this:

It will allow you to change the input parameters and see how the result of the workflow execution changes accordingly. The second form is for altering the business rules runtime. I will come back to that later.

The shared library contains two important classes - Person.cs, which is basically identical to the one from Geers' blog, and SalutationAssignmentRule.cs which is a wrapper over the rules we define, and the core of what we will expose to the end user.

The Activity project contains logic and data for executing the business rules in one of three ways:

  1. From a Workflow diagram designed with the Visual Studio Designer.
  2. From a hard-coded workflow where all the logic is written in C# code
  3. From a dynamic workflow where the workflow logic is generated from user input by C# code

#1 is cool. The designer produces XAML of the workflow (or "business rules"), which is suited for persitance and to a certain degree modification. I didn't really see my endusers editing XAML or using Visual Studio to change the business rules though, so this option was not for me.

#2 is cooler. Here I prove that the same can be done in code. Articles like DynamicAcitivty Creation and Kushal Shah's Adding Variables, Expressions and Bindings Programmatically helped me accomplish this, but the way I implemented it, the rules are pretty static.

#3: Now we're getting somewhere. By adding a layer on top of the code in #2, we can expose part of the rules to the end users. In this example I go as far as exposing the Visual Basic-style condition statements and the if-then-else-assignments to the user interface. This may be overkill for my end users, but I'm showing that the flexibility is there. See screenshot below.

In this DataGridView you can edit the rules runtime.

The implementation

I will focus on solution #3 from last chapter - where dynamic rules are used. For the other methods, download view the solution yourself (link at bottom).

First, the "dumb" business classes:

Here is the gist of Person.cs. -Only syntactically altered from Geers' blog:

public class Person
  {
     #region Properties
     public string Name { get; set; }
     public string Firstname { get; set; }
     public DateTime BirthDate { get; set; }
     public string Gender { get; set; }
     public bool Married { get; set; }
     public string Salutation { get; set; }
     // Read-only properties:
     public Int32 Age { get { return CalculateAge(); } }
     public Boolean Minor { get { return (Age < 18); } }
     // Methods
     private bool CalculateAge() { /* Implementation */ }
  }

Then there is the class for storing the business rules. I need to wrap the complex structure of activities, sequences and arguments from the framwork in a manner that is easier presented in a user interface. In this solution I bind a list of these objects directly to the datagrid.

public class SalutationAssignmentRule
    {
        public string Condition { get; set; }
        public int Priority { get; set; }
        public string ThenValue { get; set; }
        public string ElseValue { get; set; }
        public string TargetParameter { get; set; }

        public SalutationAssignmentRule(int priority, string condition, string thenValue, string elseValue = null)
        {
            TargetParameter = "Person.Salutation"; // Could be configurable
            Condition = condition;
            Priority = priority;
            ThenValue = thenValue;
            ElseValue = elseValue;
        }
    }

The ActivityLibrary projects is where the magic happens, specifically in SalutationRuleGeneratorDynamic.cs and RuleConverter.cs. The latter converts Rules in the form of SalutationAssignmentRule-objects to WF4 activity objects of type If, which are a subclass of the "everything-is-an-apple"-Action class.

RuleConverter.cs:

public class RuleConverter
  {
      internal static Activity ToIfActivity(SalutationAssignmentRule inRule)
      {
          var condition = new VisualBasicValue(inRule.Condition);
          var ifActivity = new If(new InArgument(condition));
          ifActivity.Then = new Assign
          {
              To = new OutArgument(new VisualBasicReference(inRule.TargetParameter)),
              Value = new InArgument(inRule.ThenValue)
          };
          if (inRule.ElseValue != null)
          {
              ifActivity.Else = new Assign
              {
                  To = new OutArgument(new VisualBasicReference(inRule.TargetParameter)),
                  Value = new InArgument(inRule.ElseValue)
              };
          }
          return ifActivity;
      }
  }
 

Note how the condition, which is a string sounding something like Person.Gender = "Male", is input to the If-class directly. By wrapping it in a VisualBasicValue, it will be interpreted runtime.

Now there's a caveat here: The root Workflow object needs to have a Workflow-property set, indicating that VB-statements will be present in the Workflow. A method to add such a parameter to an Activity is implemented in Common.cs:

internal static void AddVbSetting(Activity activity)
  {
      var settings = new VisualBasicSettings {
          ImportReferences = { new VisualBasicImportReference {
              Assembly = typeof(Person).Assembly.GetName().Name,
              Import = typeof(Person).Namespace } } };
      VisualBasic.SetSettings(activity, settings);
  }
 
If you look at the Visual Studio-generated XAML for the SalutationRules workflow, you will find the same setting generated there, in the tag <mva:VisualBasic.Settings>.

Going further up the implementation chain, we have the core Workflow generator class in SalutationRuleGeneratorDynamic.cs:

public static Activity CreateSalutationRules(List rules)
  {
      var inProperty = new DynamicActivityProperty
      {
          Name = "Person",
          Type = typeof(InArgument)
  
      };
      var activity = new DynamicActivity() { Properties = { inProperty } };
      Common.AddVbSetting(activity);
  
      var sequence = new Sequence();
      activity.Implementation = () => sequence;
  
      // Sort descending - those added first are lowest priority
      var sortedRules = rules.OrderByDescending(p => p.Priority).ToList();
      
      // Convert to if-activities and add to sequence
      foreach (var inRule in sortedRules)
      {
          var outRule = RuleConverter.ToIfActivity(inRule);
          sequence.Activities.Add(outRule);
      }
  
      return activity;
  }

The key thing to notice here is how I order the rules by priority to emulate the Priority property of the Rule class of WF3. When you emulate Rules by using a flat list of If-activities as I do here, you get implicit priority by the order of which you add the If's to the Sequence object.

Finally there is the invocation code. In WF3, you'd instantiate a RuleExecution class followed by an Execute. In WF4 the equivalent is the static method WorkflowInvoker.Invoke(). Again, this is a general Workflow executer meant for everyone - not just for us weirdos using WF4 as a Business Rule Engine.

private void ExecuteRules(Activity activity, Person person)
    {
        var input = new Dictionary();
        input.Add("Person", person);
        WorkflowInvoker.Invoke(activity, input);
    }

Persisting code-generated workflows

Before I round up, I'd like to mention that if you want to generate XAML from the code-generated activities for storage or transport purposes, you're just a small step away. I didn't include the code for it in my solution, but the key is to swap the DynamicActivity-instantiation in the CreateSalutationRules-method with a ActivityBuilder-class. The article Serializing workflows and Activities to and from XAML illustrates how to to this. You will have to do some minor syntactical changes in the CreateSalutationRules-method for it to compile, and also probably have to go thorugh a serialization-deserializiation-process before ending up with an executable Workflow.

In closing

To summarize - yes you can implement Rules in WF4. Is it easier than in WF3? I don't think so. Is it faster? I don't know. Microsoft says it is. Is it better? Maybe. Depends on what your needs are. In some regards, WF4 have actually less functionality than the Rules in WF3 - forward chaining of rules is an example of functionality you will NOT get by using WF4 as a BRE. In other areas, WF4 will provide you with more functionality though. The "rules" are no longer restricted to the If-then-else-structure of WF3. You can define a rule as any activity in the Workflow namespace. For the example Christophe Geers and I presented, the differences would not matter much though. Others will have to answer for more complex problem domains.

Downloads

The VS2010 solution: WF4-SalutationRules.zip


Update 25th Nov 2011: Turns out there's a better way to create the dynamic activity when you have static input- and output parameters as in this case. By creating a class that inherits Activity, you get to create the parameters as standard C# properties instead of generating InArgument-objects that must manually be added to the Properties collection of the Activity class.

Updated class for the dynamic activity: DynamicSalutationRulesActivity.cs (replaces SalutationRuleGeneratorSynamic)

Thanks goes out to Willem Meints and his blog for this knowledge :) He has a good three-post article about different ways of creating dynamic activities here: part one - two - three. (How come I find all the good articles about WF4 in the Benelux-lands? :p)


19 comments:

  1. Hello Frode! Thank you for your excellent article! Can you check the sample solution? MainForm.cs seems to be missing. Also, the SharedLibrary is targeting .NET 3.5...not sure if this matters though. Thanks again!

    ReplyDelete
  2. You're right. Thanks for pointing that out! WF4-SalutationRules.zip is now updated with MainForm cs.

    ReplyDelete
  3. I've added an update at the bottom of the article as I learned how to improve on the dynamic activity creating

    ReplyDelete
  4. Hi Frode, Thanks for this great article!

    Do you have any idea how should i store this ruleset in the database? if so, can you provide an example of the ruleset format in xml? Thanks

    ReplyDelete
  5. Anon Nov 27:

    Well, since I've wrapped the rules in the business class SalutationAssignmentRule, I would store instances of that class to a database. Why not just create a table called SalutationAssignmentRule, map each property to a column and use Linq2SQL or EF to read/write? Keep it simple.

    The business case I'm outlining assumes that the rules themselves are in a set format (IF/Then/Else-assignment). If you want even more dynamic rules, you would need a more dynamic editor and more dynamic persistence - maybe serializing the workflow directly as XML. See "Serializing Workflows and Activities to and from XAML" http://msdn.microsoft.com/en-us/library/ff458319.aspx . But that is a different business case.

    ReplyDelete
  6. Hi Frode,
    Thanks for your prompt reply. I am actually doing some research on workflow features. trying to apply workflow on my coming project. I will be doing a project for e-forms approval system. There will be many e-forms, each of the eform have different approver and approval routing. do you think workflow can handle this? any advice? thanks in advance.

    ReplyDelete
  7. Yea I don't see why workflow wouldn't handle the case you describe. You would need a different workflow (activity diagram) for each type of e-form then.

    Use workflows when you need your business rules to be dynamic, or when you want your end users to edit them.

    Another reason could be that you simply like the visualization you get from "workflowifying" your logic. It adds clarity and could be used in documentation and dialog with the non-technical stakeholders.

    ReplyDelete
  8. I am thinking one workflow for all the eforms.
    still in doing research. long way to go. :p

    ReplyDelete
  9. Nice article. Regards, Christophe

    ReplyDelete
  10. Frode,

    Nice article - thanks very much. As you say, this is not a full replacement for a Rete-based BRE, but it does meet some of the frequent requirements within business systems - a simple set of sequential, ordered, If ... Then ... Else conditions.

    One thing that I did want to ask though - if I understand your model correctly, this is designed to be deployed as a code module. Have you given any thought to how this might be deployed as a Workflow Service at all?

    Paul Marriott
    Technical Director
    MKM Health

    ReplyDelete
  11. Hi Paul - I have to admin I have zero experience with Workflow Services. I have deployed a service based on these concepts as a WCF service though, where the workflow is a code module (DLL) referred by the service module. Maybe someone else can shed some light on this?

    ReplyDelete
  12. Hi frode, two quick questions, currently the code will execute the rules until it finds the first match, then stops, what parts would i need to change to make it systematically pass through each rule? Additionally is it possibble to make the output of the rule, the then and else outputs, perform an Add to a property, e.g adding 3 to a property rather than overwriting and setting to a 3.

    Other than that fantastic code, really helped me set up my rule engine, better than most tutorials out there!

    ReplyDelete
  13. Anon July 17: Actually you're wrong. The current code _will_ execute all rules even if the first matches! The scenario I describe work because of the priorization - it executes from the highest number to the lowest. So for a male minor in this example, the rule with priorty 30 will first hit and assign the salutation to "Mr", but then an instant later it will be overwritten with "To the parents of" by the rule with priorty 10. I should probably have outlined this better in the article.

    As for adding a value to a property instead of assigning, you could still use the assignment Action, but use the current value as input to the next. Basically like "theValue = theValue + 1".

    For more complex operations, you can look to the InvokeMethod-Action, which basically allows you to invoke any C#-method. See http://msdn.microsoft.com/en-us/library/dd807388.aspx

    ReplyDelete
    Replies
    1. Frode, do you know of a way to abort checking against the rest of the rules once one of them fails? e.g 4 rules, second one fails so abort and dont attempt number 3 or 4. Something like this i feel would be very helpful as were looking to rack up a good few hundred rules and dont want to keep testing rules when we know the object failed early on.

      Ta in advance, Anon July 17

      Delete
    2. One approach is to attach each rule to the Else-branch of the previous one. You might have to rewrite some rules to accommodate this approach. In the scenario above, you'd have to add a fourth rule saying something like "If Person.Gender <> 'Male' Then 'Miss' "

      Another approach might be to wrap all assignment operations (Then-branch) in a Sequence ending with a TerminateActivity (http://msdn.microsoft.com/en-us/library/ms734509%28v=vs.90%29)

      Delete
  14. Frode, ah yes my mistake, moments after posting i noticed that my test data, ONLY matched ONE rule anyway so never hit any others, my mistake!

    Yes i thought id be able to do that too but it repeatedly failed, i had no way of finding what was already there by the time i was that deep through the code and wasn't in the mood to re-do every level to pass the value down that far. instead i just told the class that if its given an int add it to what its already got by using the set part of the property, was much quicker and works regardless.

    Thankyou for the fast reply and the links! i shall begin scrollign through it all now. Much thanks!

    Anon July 17

    ReplyDelete
  15. Hi Frode
    Extremely useful article thanks. How could I extend the rule conditions beyond simple logic operators to include functions such as "if(Mystring.contains("Priority")) then ..."?

    ReplyDelete
  16. Hello Frode,

    FYI, I noticed that the rule sequence created in Visual Studio does not function correctly for the case of Female and Married. The result is Miss instead of Mrs. It appears to stop evaluation after the Else assignment in the first If condition.

    ReplyDelete
  17. This comment has been removed by the author.

    ReplyDelete