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:
Set(x=>x.Target)
.With(x=> ComputeValue(x))
.If(x => SomeConditionOn(x))
.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:
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.
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:
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:
abcd.Target.A = 1;
Checking the state of the object:
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.
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):
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);
}
...
}
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.
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