From 63ecbe6a644be0a91673fd19d9cfaa7f8e46f16f Mon Sep 17 00:00:00 2001 From: Dishant Munjal Date: Tue, 24 Sep 2019 08:53:33 +0530 Subject: [PATCH] including exceptions scenario (#2) * including exceptions scenario * Unit Test Cases --- src/RulesEngine/RulesEngine.sln | 3 + .../ExpressionBuilders/LambdaExpressionBuilder.cs | 17 ++++- .../RulesEngine/HelperFunctions/Helpers.cs | 14 ++-- .../RulesEngine/Models/RuleResultTree.cs | 5 ++ .../RulesEngine.UnitTest/BusinessRuleEngineTest.cs | 88 +++++++++++++++------- .../RulesEngine.UnitTest.csproj | 3 + test/RulesEngine.UnitTest/TestData/rules1.json | 17 ++--- test/RulesEngine.UnitTest/TestData/rules2.json | 27 ++++--- test/RulesEngine.UnitTest/TestData/rules3.json | 13 ++++ 9 files changed, 124 insertions(+), 63 deletions(-) create mode 100644 test/RulesEngine.UnitTest/TestData/rules3.json diff --git a/src/RulesEngine/RulesEngine.sln b/src/RulesEngine/RulesEngine.sln index a4a0c6c..7ae0121 100644 --- a/src/RulesEngine/RulesEngine.sln +++ b/src/RulesEngine/RulesEngine.sln @@ -8,6 +8,9 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngine.UnitTest", "..\..\test\RulesEngine.UnitTest\RulesEngine.UnitTest.csproj", "{50E0C2A5-E2C8-4B12-8C0E-B69F698A82BF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DemoApp", "..\..\demo\DemoApp\DemoApp.csproj", "{57BB8C07-799A-4F87-A7CC-D3D3F694DD02}" + ProjectSection(ProjectDependencies) = postProject + {CD4DFE6A-083B-478E-8377-77F474833E30} = {CD4DFE6A-083B-478E-8377-77F474833E30} + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs index 7ee6df2..ce13397 100644 --- a/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs +++ b/src/RulesEngine/RulesEngine/ExpressionBuilders/LambdaExpressionBuilder.cs @@ -26,10 +26,19 @@ namespace RulesEngine.ExpressionBuilders } internal override Expression> BuildExpressionForRule(Rule rule, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp) { - var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; - var e = DynamicExpressionParser.ParseLambda(config, typeParamExpressions.ToArray(), null, rule.Expression); - var body = (BinaryExpression)e.Body; - return Helpers.ToResultTreeExpression(rule, null, body, typeParamExpressions, ruleInputExp); + try + { + var config = new ParsingConfig { CustomTypeProvider = new CustomTypeProvider(_reSettings.CustomTypes) }; + var e = DynamicExpressionParser.ParseLambda(config, typeParamExpressions.ToArray(), null, rule.Expression); + var body = (BinaryExpression)e.Body; + return Helpers.ToResultTreeExpression(rule, null, body, typeParamExpressions, ruleInputExp); + } + catch (Exception ex) + { + var binaryExpression = Expression.And(Expression.Constant(true), Expression.Constant(false)); + var exceptionMessage = ex.Message; + return Helpers.ToResultTreeExpression(rule, null, binaryExpression, typeParamExpressions, ruleInputExp, exceptionMessage); + } } } } diff --git a/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs b/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs index 222a27d..0446b44 100644 --- a/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs +++ b/src/RulesEngine/RulesEngine/HelperFunctions/Helpers.cs @@ -23,9 +23,9 @@ namespace RulesEngine.HelperFunctions /// The type parameter expressions. /// The rule input exp. /// Expression of func - internal static Expression> ToResultTreeExpression(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp) + internal static Expression> ToResultTreeExpression(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, ParameterExpression ruleInputExp, string exceptionMessage = "") { - var memberInit = ToResultTree(rule, childRuleResults, isSuccessExp, typeParamExpressions, null); + var memberInit = ToResultTree(rule, childRuleResults, isSuccessExp, typeParamExpressions, null, exceptionMessage); var lambda = Expression.Lambda>(memberInit, new[] { ruleInputExp }); return lambda; } @@ -38,7 +38,7 @@ namespace RulesEngine.HelperFunctions /// The is success exp. /// The child rule results block expression. /// - internal static MemberInitExpression ToResultTree(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, BlockExpression childRuleResultsblockexpr) + internal static MemberInitExpression ToResultTree(Rule rule, IEnumerable childRuleResults, BinaryExpression isSuccessExp, IEnumerable typeParamExpressions, BlockExpression childRuleResultsblockexpr, string exceptionMessage = "") { var createdType = typeof(RuleResultTree); var ctor = Expression.New(createdType); @@ -47,10 +47,12 @@ namespace RulesEngine.HelperFunctions var isSuccessProp = createdType.GetProperty(nameof(RuleResultTree.IsSuccess)); var childResultProp = createdType.GetProperty(nameof(RuleResultTree.ChildResults)); var inputProp = createdType.GetProperty(nameof(RuleResultTree.Input)); + var exceptionProp = createdType.GetProperty(nameof(RuleResultTree.ExceptionMessage)); var rulePropBinding = Expression.Bind(ruleProp, Expression.Constant(rule)); var isSuccessPropBinding = Expression.Bind(isSuccessProp, isSuccessExp); var inputBinding = Expression.Bind(inputProp, typeParamExpressions.FirstOrDefault()); + var exceptionBinding = Expression.Bind(exceptionProp, Expression.Constant(exceptionMessage)); MemberInitExpression memberInit; @@ -59,16 +61,16 @@ namespace RulesEngine.HelperFunctions var ruleResultTreeArr = Expression.NewArrayInit(typeof(RuleResultTree), childRuleResults); var childResultPropBinding = Expression.Bind(childResultProp, ruleResultTreeArr); - memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding }); + memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding, exceptionBinding }); } else if (childRuleResultsblockexpr != null) { var childResultPropBinding = Expression.Bind(childResultProp, childRuleResultsblockexpr); - memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding }); + memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, childResultPropBinding, inputBinding, exceptionBinding }); } else { - memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, inputBinding }); + memberInit = Expression.MemberInit(ctor, new[] { rulePropBinding, isSuccessPropBinding, inputBinding, exceptionBinding }); } return memberInit; diff --git a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs b/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs index fc00462..8037f69 100644 --- a/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs +++ b/src/RulesEngine/RulesEngine/Models/RuleResultTree.cs @@ -40,6 +40,11 @@ namespace RulesEngine.Models /// public object Input { get; set; } + /// + /// Gets the exception message in case an error is thrown during rules calculation. + /// + public string ExceptionMessage { get; set; } + /// /// This method will return all the error and warning messages to caller /// diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index cf13e7f..5179202 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -13,6 +13,7 @@ using System.Dynamic; using System.IO; using System.Linq; using Xunit; +using Newtonsoft.Json.Converters; namespace RulesEngine.UnitTest { @@ -32,12 +33,12 @@ namespace RulesEngine.UnitTest public void RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput(); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); - dynamic input2 = GetInput(); - input2.value1 = "val1"; - - var result = re.ExecuteRule("inputWorkflowReference", new List() { input, input2 }.AsEnumerable(), new object[] { }); + var result = re.ExecuteRule("inputWorkflowReference", new List() { input1, input2, input3 }.AsEnumerable(), new object[] { }); Assert.NotNull(result); Assert.IsType>(result); } @@ -47,12 +48,12 @@ namespace RulesEngine.UnitTest public void ExecuteRule_ReturnsListOfRuleResultTree(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput(); - dynamic input2 = GetInput(); - input2.value1 = "val1"; + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); - var result = re.ExecuteRule("inputWorkflow", new List() { input, input2 }.AsEnumerable(), new object[] { }); + var result = re.ExecuteRule("inputWorkflow", new List() { input1, input2, input3 }.AsEnumerable(), new object[] { }); Assert.NotNull(result); Assert.IsType>(result); } @@ -62,27 +63,43 @@ namespace RulesEngine.UnitTest public void ExecuteRule_SingleObject_ReturnsListOfRuleResultTree(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput(); - dynamic input2 = GetInput(); - input2.value1 = "val1"; + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); - var result = re.ExecuteRule("inputWorkflow",input); + var result = re.ExecuteRule("inputWorkflow",input1); Assert.NotNull(result); Assert.IsType>(result); } + [Theory] + [InlineData("rules3.json")] + public void ExecuteRule_ExceptionScenario_RulesInvalid(string ruleFileName) + { + var re = GetRulesEngine(ruleFileName); + + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); + + var result = re.ExecuteRule("inputWorkflow", new List() { input1, input2, input3 }.AsEnumerable(), new object[] { }); + Assert.NotNull(result); + Assert.IsType>(result); + Assert.False(string.IsNullOrEmpty(result[0].ExceptionMessage) || string.IsNullOrWhiteSpace(result[0].ExceptionMessage)); + } + [Theory] [InlineData("rules2.json")] public void ExecuteRule_ReturnsListOfRuleResultTree_ResultMessage(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput(); - dynamic input2 = GetInput(); - input2.value1 = "val1"; + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); - List result = re.ExecuteRule("inputWorkflow", input); + var result = re.ExecuteRule("inputWorkflow", new List() { input1, input2, input3 }.AsEnumerable(), new object[] { }); Assert.NotNull(result); Assert.IsType>(result); Assert.NotNull(result.First().GetMessages()); @@ -116,7 +133,7 @@ namespace RulesEngine.UnitTest public void ExecuteRule_InvalidWorkFlow_ThrowsException(string ruleFileName) { var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput(); + dynamic input = GetInput1(); Assert.Throws(() => { re.ExecuteRule("inputWorkflow1", new List() { input }.AsEnumerable(), new object[] { }); }); } @@ -128,11 +145,11 @@ namespace RulesEngine.UnitTest { var re = GetRulesEngine(ruleFileName); - dynamic input = GetInput(); - dynamic input2 = GetInput(); - input2.valueX = "hello"; + dynamic input1 = GetInput1(); + dynamic input2 = GetInput2(); + dynamic input3 = GetInput3(); - var result = re.ExecuteRule("inputWorkflow", new List() { input, input2 }.AsEnumerable(), new object[] { }); + var result = re.ExecuteRule("inputWorkflow", new List() { input1, input2, input3 }.AsEnumerable(), new object[] { }); Assert.NotNull(result); Assert.IsType>(result); } @@ -159,14 +176,27 @@ namespace RulesEngine.UnitTest return new RulesEngine(new string[] { data, injectWorkflowStr}, mockLogger.Object); } - private dynamic GetInput() + + private dynamic GetInput1() { - dynamic input = new ExpandoObject(); - input.value1 = "value1"; - input.value2 = "value2"; - input.value3 = 1; - input.value4 = new { subValue = "subValue", nullValue = default(string) }; - return input; + var converter = new ExpandoObjectConverter(); + var basicInfo = "{\"name\": \"Dishant\",\"email\": \"dishantmunjal@live.com\",\"creditHistory\": \"good\",\"country\": \"canada\",\"loyalityFactor\": 3,\"totalPurchasesToDate\": 10000}"; + return JsonConvert.DeserializeObject(basicInfo, converter); } + + private dynamic GetInput2() + { + var converter = new ExpandoObjectConverter(); + var orderInfo = "{\"totalOrders\": 5,\"recurringItems\": 2}"; + return JsonConvert.DeserializeObject(orderInfo, converter); + } + + private dynamic GetInput3() + { + var converter = new ExpandoObjectConverter(); + var telemetryInfo = "{\"noOfVisitsPerMonth\": 10,\"percentageOfBuyingToVisit\": 15}"; + return JsonConvert.DeserializeObject(telemetryInfo, converter); + } + } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index 06b40a5..324c105 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -23,6 +23,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/test/RulesEngine.UnitTest/TestData/rules1.json b/test/RulesEngine.UnitTest/TestData/rules1.json index 53acad6..69b06e0 100644 --- a/test/RulesEngine.UnitTest/TestData/rules1.json +++ b/test/RulesEngine.UnitTest/TestData/rules1.json @@ -2,19 +2,12 @@ "WorkflowName": "inputWorkflow", "Rules": [ { - "RuleName": "Rule1", - "Operator": "AndAlso", + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", "ErrorMessage": "One or more adjust rules failed.", "ErrorType": "Error", - "Rules": [ - { - "RuleName": "SubRule1", - "Expression": "input1.Request_RequestType == \"vod\" AND input1.Labor_BillingCode == \"billable\" AND ((input1.Request_RegistrationStatus == \"cancelled with t&e\" AND input1.Request_Status == \"cancelled\") OR (input1.Request_Status != \"cancelled\"))", - "ErrorMessage": "SubError message 1", - "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression" - } - ] + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" } ] -} +} \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/TestData/rules2.json b/test/RulesEngine.UnitTest/TestData/rules2.json index 9ea5aa1..b94a55d 100644 --- a/test/RulesEngine.UnitTest/TestData/rules2.json +++ b/test/RulesEngine.UnitTest/TestData/rules2.json @@ -8,25 +8,28 @@ "ErrorType": "Error", "Rules": [ { - "RuleName": "SubRule1", - "Expression": "input1.Request_RequestType == \"vod\" AND input1.Labor_BillingCode == \"billable\" AND ((input1.Request_RegistrationStatus == \"cancelled with t&e\" AND input1.Request_Status == \"cancelled\") OR (input1.Request_Status != \"cancelled\"))", - "ErrorMessage": "SubError message 1", + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression" + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" }, { - "RuleName": "SubRule2", - "Expression": "1 == 1", - "ErrorMessage": "SubError message 2", + "RuleName": "GiveDiscount20", + "SuccessEvent": "20", + "ErrorMessage": "One or more adjust rules failed.", "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression" + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country == \"india\" AND input1.loyalityFactor == 3 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" }, { - "RuleName": "SubRule3", - "Expression": "input1.Request_RequestType == \"vod\" AND input1.Labor_BillingCode == \"billable\" AND ((input1.Request_RegistrationStatus == \"cancelled with t&e\" AND input1.Request_Status == \"cancelled\") OR (input1.Request_Status != \"cancelled\"))", - "ErrorMessage": "SubError message 3", + "RuleName": "GiveDiscount25", + "SuccessEvent": "25", + "ErrorMessage": "One or more adjust rules failed.", "ErrorType": "Error", - "RuleExpressionType": "LambdaExpression" + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.country != \"india\" AND input1.loyalityFactor >= 2 AND input1.totalPurchasesToDate >= 10000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 5" } ] } diff --git a/test/RulesEngine.UnitTest/TestData/rules3.json b/test/RulesEngine.UnitTest/TestData/rules3.json new file mode 100644 index 0000000..f49936b --- /dev/null +++ b/test/RulesEngine.UnitTest/TestData/rules3.json @@ -0,0 +1,13 @@ +{ + "WorkflowName": "inputWorkflow", + "Rules": [ + { + "RuleName": "GiveDiscount10", + "SuccessEvent": "10", + "ErrorMessage": "One or more adjust rules failed.", + "ErrorType": "Error", + "RuleExpressionType": "LambdaExpression", + "Expression": "input1.couy == \"india\" AND input1.loyalityFactor <= 2 AND input1.totalPurchasesToDate >= 5000 AND input2.totalOrders > 2 AND input3.noOfVisitsPerMonth > 2" + } + ] +} \ No newline at end of file