Introduction
Architecture has an elegance and a purpose. For a given set of data, consider rows of a table consisting of meaningful data. Like in a loan, a financial product, there can be a number of different types of rules, like the total amount should be greater than 10000, the total amount should be greater than the balance amount, client name should not be blank and so on. Architecture in this article assumes that assorted such rules can be classified in classes and can be reused many times later, and if the need arises, the new type of rules can be created and integrated into the application. In this article, I will discuss only two types of rules: validation of a field and comparison of any two fields of the same dataset.
Approach
There are mainly 4 classes in this small application.
- FpProduct, representing a financial product, this represents entity.
- FpProductsEntities, this represents the collection of financial product ie. FpProduct.
- ClassVisitValidateField to validate the fields of FpProduct in collection named FpProductsEntities
- ClassVisitCompareFields to compare between two fields of FpProduct in FpProductsEntities.
The 3 and 4 Classes are rules based upon the FpProductsEntities that will be filtered.
The desire is to keep rules unknowledgeable to fields of entity upon which it will apply, and that made the reason to leverage the expression tree to achieve the same.
Code
Below is the implementation of Classes and Interfaces used in the application.
-
- class FpProduct
- {
- public string ProductiD { get; set; }
- public string ProductName { get; set; }
- public string ProductType { get; set; }
- public double ProductTotalAmount { get; set; }
- public double ProductBalanceAmount { get; set; }
- public int ProductTenure { get; set; }
-
- public FpProduct(string Productid, string ProdName, string ProdType, double ProdTotalAmount,
- double ProdBalanceAmount, int ProdTenure)
- {
- ProductiD = Productid;
- ProductName = ProdName;
- ProductType = ProdType;
- ProductTotalAmount = ProdTotalAmount;
- ProductBalanceAmount = ProdBalanceAmount;
- ProductTenure = ProdTenure;
- }
-
- public override string ToString()
- {
- return "Product id: "+ProductiD + " Product Name:" + ProductName+ " Product Type: "+ ProductType +
- " Product Total Amount: "+ProductTotalAmount+ " Product Balance Amount: "+ProductBalanceAmount + " Product Tenure: " +ProductTenure;
- }
- }
-
-
- // the rules.Few things needed to note about the Class are:
-
-
-
- just next
- // to the definition of this Class
-
- class FpProductsEntities :InterfaceProductEntities<FpProduct>
- {
- public List<FpProduct> ListFpProduct { get; set; }
- public List<InterfaceVisitor<FpProduct>> RuleDirectory { get; set; }
- public void AttachRule(InterfaceVisitor<FpProduct> Rule)
- {
- if (RuleDirectory == null)
- RuleDirectory = new List<InterfaceVisitor<FpProduct>>();
-
- RuleDirectory.Add(Rule);
- }
- public void LoadDate()
- {
- if (ListFpProduct == null)
- {
- ListFpProduct = new List<FpProduct>();
- }
- ListFpProduct.Add(new FpProduct("Prd1", "Loan0Tom2019", "Loan", 2000, 1500, 1));
- ListFpProduct.Add(new FpProduct("Prd2", "Loan0Moody2017", "USLease", 12000, 150, 1));
- ListFpProduct.Add(new FpProduct("Prd3", "Loan0Tom2018", "SLoan", 2000, 1500, 1));
- ListFpProduct.Add(new FpProduct("Prd4", "Loan0Rex2018", "LLoan", 20000, 1500, 1));
- ListFpProduct.Add(new FpProduct("Prd5", "Loan0Ahuja2020", "CarLoan", 9000, 15000, 1));
- ListFpProduct.Add(new FpProduct("Prd6", "Loan0Pat2019", "Loan", 7000, 7500, 1));
- }
- }
-
- interface InterfaceProductEntities<T>
- {
- List<T> ListFpProduct { get; set; }
- List<InterfaceVisitor<T>> RuleDirectory { get; set; }
- }
We defined how our entity and collection of entities will look like,now lets see the rules.For now I have added only two rule types.
- ClassVisitValidateField
This rule will compare field value with a constant. For example, a rule of this type will be described: "ProductTotalAmount should be greater than 10000".
- ClassVisitCompareFields
This will compare the values of two fields like ProductTotalAmount should be greater than ProductBalanceAmount.
Based upon the criteria set by rules, the result set will be filtered out as output, which we will see later.
These classes are as follows
-
- class ClassVisitValidateField<T> : InterfaceVisitor<T>
- {
- public string _ruleiD { get; set; }
- public string _propertyName { get; set; }
- public ExpressionType _ExpOperator {get ; set;}
- public string _PropertyValue { get; set; }
-
- public ClassVisitValidateField(string ruleid, string PropertyName, ExpressionType exp, string PropertyValue)
- {
- _ruleiD = ruleid;
- _propertyName = PropertyName;
- _ExpOperator = exp;
- _PropertyValue = PropertyValue;
- }
- public Func<T, bool> Visit()
- {
- var parameterExpression = Expression.Parameter(typeof(T), "FinanacialProduct");
- var property = Expression.Property(parameterExpression, _propertyName);
- var propertyType = typeof(T).GetProperty(_propertyName).PropertyType;
- var constant = Expression.Constant(Convert.ChangeType(_PropertyValue, propertyType));
- var binaryExpression = Expression.MakeBinary(_ExpOperator, property, constant);
- var lambda = Expression.Lambda<Func<T, bool>>(binaryExpression, parameterExpression);
- return lambda.Compile();
- }
-
- public override string ToString()
- {
- return "Rule Id: " + _ruleiD + ">" + _propertyName + " Should be " + _ExpOperator.ToString() +" " + _PropertyValue;
- }
-
-
- class ClassVisitCompareFields<T> : InterfaceVisitor<T>
- {
- public string _ruleid { get; set; }
- public string _leftProperty { get; set; }
- public string _rightProperty { get; set; }
- public double _factorProperty { get; set; }
- public ExpressionType _ExpOperator {get ; set;}
-
- public ClassVisitCompareFields(string ruleId ,string leftField,ExpressionType expOperator,double factor,string rightProperty)
- {
- _ruleid=ruleId;
- _leftProperty=leftField;
- _ExpOperator=expOperator;
- _factorProperty =factor;
- _rightProperty=rightProperty;
- }
- public Func<T, bool> Visit()
- {
- var parameterExpression = Expression.Parameter(typeof(T), "FinanacialProduct");
- var propertyleft = Expression.Property(parameterExpression, _leftProperty);
- var propertyRight = Expression.Property(parameterExpression, _rightProperty);
- var binaryRight = Expression.Multiply(propertyRight, Expression.Constant(_factorProperty));
- var binaryExpression = Expression.MakeBinary(_ExpOperator, propertyleft, binaryRight);
- var lambda = Expression.Lambda<Func<T, bool>>(binaryExpression, parameterExpression);
- return lambda.Compile();
- }
- public override string ToString()
- {
- return "Rule Id: " + _ruleid + ">" + _leftProperty + " Should be " + _ExpOperator.ToString() + " " + _factorProperty + " times " + _rightProperty;
- }
- }
Note that both rules types implement an interface InterfaceVisitor.This interface defines a method Visit. This method actually creates the compiled lambda expression. Both above rules, if you notice, implement this method differently. So if we need another type of rule besides them. We need to implement this method to create the lambda which will represent the rule. The definition of InterfaceVisitor is as follows,
-
- interface InterfaceVisitor<T>
- {
- Func<T, bool> Visit();
- }
Our wagon is ready; we only now need to pull them. Below is the Main function in Program.cs which will make use of the above classes and then delegate the run to a different class named MainEngine.cs. The reason behind this class is the separation of concern. Let’s see the Main method of Program.cs and the MainEngine.cs.
- static void Main(string[] args)
- {
-
- FpProductsEntities fpent = new FpProductsEntities();
-
- fpent.LoadDate();
-
- fpent.AttachRule(new ClassVisitValidateField<FpProduct>("Rule1", "ProductTotalAmount", ExpressionType.GreaterThanOrEqual, "2000"));
- fpent.AttachRule(new ClassVisitCompareFields<FpProduct>("Rule2","ProductTotalAmount",ExpressionType.LessThanOrEqual,1,"ProductBalanceAmount"));
- fpent.AttachRule(new ClassVisitCompareFields<FpProduct>("Rule2", "ProductTotalAmount", ExpressionType.GreaterThan ,1.5, "ProductBalanceAmount"));
-
- MainEngine.Instance().RunRule(fpent);
- Console.ReadLine();
- }
Below is the MainEngine.cs where actual filtered result sets are returned and displayed.
- class MainEngine {
- private static MainEngine _Instance;
- protected MainEngine() {}
- public static MainEngine Instance() {
- if (_Instance == null) {
- _Instance = new MainEngine();
- }
- return _Instance;
- }
-
- public void RunRule < T > (InterfaceProductEntities < T > fp) {
- foreach(var rule in fp.RuleDirectory) {
- Console.WriteLine("\n" + rule.ToString());
- Console.WriteLine("-----------------------------------------------");
- var result = fp.ListFpProduct.Where(rule.Visit());
- foreach(var filteredProduct in result)
- Console.WriteLine(filteredProduct.ToString());
- Console.WriteLine("------------------------------------------------");
- }
Conclusion
This is a simple rule engine with limited functionalities. The idea is to show that for a fixed dataset there can be multiple types of rules, each require different set of input, hence different implementation. The good thing is that chances of reusability are far greater than the need for additional new rule types. The second thing is that we don’t need data to create the rule as we have seen that implemented rules above use generic type to create the lambda expression.
The downside of this implementation is run time error. What if property name during the instantiation of a rule is misspelled? But that issue comes up with any dynamic implementation; it can be handled at the UI level.