Click here to Skip to main content
15,885,244 members
Articles / Programming Languages / C#

BusinessRulesEngine for .NET Applications

Rate me:
Please Sign up or sign in to vote.
5.00/5 (13 votes)
1 Dec 2019CPOL3 min read 16.3K   22   7
Complex cascade defaulting of properties on an object graph triggered by other property changes

Introduction

The project is fully open source and already used in highly demanding production environments.

The sources are here. The package is available on nuget.org.

dotnet add package BusinessRules.Engine --version 1.0.7

or:

Install-Package BusinessRules.Engine -Version 1.0.7

In the last version, I simplified the fluent syntax to define a rule. No need of "EndRule" and "Or" any more. A rule is defined like this:

C#
Set(x=>x.Target)

 .With(x=> ComputeValue(x))

 .If(x => SomeConditionOn(x))

  // variable number of arguments for OnChanged
 .OnChanged(x=> x.SomePropertyOnWhichTargetDepends, x=> SomeOtherProperty); 

The Problem

I was motivated to develop this component by a real-life problem. Very complex cascading rules are required by a trade processing system. It is already used in production environment (investment banking). The same need may arrive in a large category of business-line systems. Given on object graph, when a property of an object is changed, it triggers cascading changes (defaulting of values dependent on other properties) in the whole graph. Naïve approaches simply do not work. Triggering the business rules inside the setters, or worse, coding the business rules inside the setters. Seems straight-forward, but it is not maintainable in the long run:

  • Business rules are strongly coupled with data structure: this does not allow to apply different rules depending on the context
  • Business rules are spread all over the object graph which makes long-term maintenance a nightmare (I really had to do it on a legacy system, it is not hypothetical)
  • Very easy to trigger the same rule multiple times: performance issue and debugging nightmare
  • In most cases, the value to be defaulted depends on multiple inputs. There is no guarantee that the code will behave the same way if the inputs are changed in different order. Debugging and maintenance nightmare again.

The Solution

The solution was inspired by the ORM frameworks that need to intercept property changes to generate SQL update commands; a transparent proxy is created around an object, it intercepts changes and stores them. That will generate a SQL UPDATE command when the object is saved.

In a similar way, in our case, a transparent proxy will be created around each object in the graph. It intercepts changes and triggers the required rules. Let’s see some code:

To start, an abstract example involving a single object:

C#
public class Abcd : IAbcd
{
    public int A { get; set; }
    public int B { get; set; }
    public int C { get; set; }
    public int D { get; set; }
}

Rules are expressed in a different class. They are all described in the constructor of this class using a fluent syntax.

C#
public class AbcdRules : MappingRules<IAbcd>
{
   public AbcdRules()        
   {
       Set(x => x.B)
           .With(x => x.A)
           .If(x => x.A < 100);
           .OnChanged(x => x.A);

       Set(x => x.C)
           .With(x => x.B)
           .If(x => x.C < 100)
           .OnChanged(x => x.B);

       Set(x => x.D)
           .With(x => x.C)
           .If(x => x.D < 100)
           .OnChanged(x => x.C);           

       Set(x => x.A)
           .With(x => x.D + 1)
           .If(x => x.A < 100)
           .OnChanged(x => x.D);
   } 

In order to use the rules engine, we instantiate a "facade". It takes an instance of the object as parameter and an instance of the rules:

C#
var instance = new Abcd();

var rules = new AbcdRules()

var abcd = new InterfaceWrapper<IAbcd>(instance, rules);

We set values on the facade as if it was our business object.

Setting one value:

C#
abcd.Target.A = 1;

Checking the state of the object:

C#
Assert.AreEqual(100, instance.A);

Intercepting Property Updates

To create a typed proxy around an object, all public properties need to be virtual (explicitly declared virtual or inherited from an interface as in the previous example) and the class should not be sealed.

An alternative solution is proposed when these conditions cannot be met: using a dynamic proxy.

An example from a real trading system. This time, we create a facade around an object graph containing two nodes trade -> product. Setting a property on an object may change the other.

C#
var trade = new CdsTrade
{
    Product = new CreditDefaultSwap()
};

var rules = new CdsRules();

dynamic p = new DynamicWrapper<CdsTrade>(trade, rules);

p.CdsProduct.RefEntity = "AXA";

p.Counterparty = "CHASEOTC";

Assert.AreEqual("ICEURO", trade.ClearingHouse);
Assert.AreEqual("MMR", trade.CdsProduct.Restructuring);
Assert.AreEqual("SNR", trade.CdsProduct.Seniority);

A small excerpt from the rules of a system in production (the complete code uses around 300 rules):

C#
public class CdsRules : MappingRules<CdsTrade>
{
    public CdsRules()        
    {
        Set(t => t.CounterpartyRole)
            .With(t => t.Sales != null ? "Client" : "Dealer")
            .OnChanged(t => t.Sales);

        Set(t => t.ClearingHouse)
            .With(t => GetDefaultClearingHouse(t.Counterparty, t.CdsProduct.RefEntity))
            .OnChanged(t => t.CdsProduct.RefEntity, t => t.Counterparty);

        Set(t => t.SalesCredit)
            .With(t => Calculator(t.CdsProduct.Spread, t.CdsProduct.Nominal))
            .OnChanged(t => t.CdsProduct.Spread, t => t.CdsProduct.RefEntity);

        Set(t => t.CdsProduct.TransactionType)
            .With(t => GetTransactionType(t.CdsProduct.RefEntity))
            .OnChanged(t => t.CdsProduct.RefEntity);

        Set(t => t.CdsProduct.Currency)
            .With(t => GetDefaultCurrency(t.CdsProduct.TransactionType))
            .OnChanged(t => t.CdsProduct.TransactionType);

        Set(t => t.CdsProduct.Restructuring)
            .With(t => GetDefaultRestructuring(t.CdsProduct.TransactionType))
            .OnChanged(t => t.CdsProduct.TransactionType);

        Set(t => t.CdsProduct.Seniority)
            .With(t => GetDefaultSeniority(t.CdsProduct.TransactionType))
            .OnChanged(t => t.CdsProduct.TransactionType);
    }
    // more code here    
    ...
}

Both facades implement INotifyPropertyChange so they can be directly data-bound to a WPF or WindowsForms view.

Internally, all property updates are done with code injection (no reflection). As you can see in the performance test, it is blazing fast.

Practical Advice

  • A rule is triggered only if the value of the property changes. For value types, to distinguish between a property which has its default value and one that is not filled, use Nullable types.
  • If you want to force a target value in some condition, put it in the triggers too. Thus, if another value is set after the rule is triggered, it will be triggered again and override the value.
C#
Set(v => v.DealWay)
  .With(v => "B")
  .If(v => v.BookOcCode == 9826 && v.Folder == "HEDGE_OA_HY")
  .OnChanged(v => v.BookOcCode)
  .Or(v => v.Folder)
  .Or(v=>v.DealWay)
.EndRule();

History

  • 21st November, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Europe Europe
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow does this compare to CSLA.net? Pin
rhyous2-Dec-19 10:57
rhyous2-Dec-19 10:57 
QuestionRedux Pin
Damon Wilder Carr2-Dec-19 10:22
Damon Wilder Carr2-Dec-19 10:22 
AnswerRe: Redux Pin
Dan Ionescu (USINESOFT)2-Dec-19 19:54
Dan Ionescu (USINESOFT)2-Dec-19 19:54 
QuestionThank You! Pin
Hyland Computer Systems22-Nov-19 19:59
Hyland Computer Systems22-Nov-19 19:59 
AnswerRe: Thank You! Pin
Dan Ionescu (USINESOFT)24-Nov-19 22:09
Dan Ionescu (USINESOFT)24-Nov-19 22:09 
GeneralRe: Thank You! Pin
Hyland Computer Systems25-Nov-19 16:20
Hyland Computer Systems25-Nov-19 16:20 
GeneralRe: Thank You! Pin
Dan Ionescu (USINESOFT)25-Nov-19 19:28
Dan Ionescu (USINESOFT)25-Nov-19 19:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.