From b763f718bc27dce420c37d14b4ec8d709a6470be Mon Sep 17 00:00:00 2001 From: Abbas Cyclewala Date: Tue, 20 Jul 2021 16:54:32 +0530 Subject: [PATCH] Abbasc52/actions for nested levels (#182) * Added support for nested rule actions * Changed type for Actions --- CHANGELOG.md | 9 ++ RulesEngine.sln | 1 + schema/workflowRules-schema.json | 38 ++++++++- src/RulesEngine/Actions/ActionContext.cs | 13 ++- src/RulesEngine/Enums/ActionTriggerType.cs | 11 --- src/RulesEngine/Models/Rule.cs | 3 +- src/RulesEngine/Models/RuleAction.cs | 14 ++++ src/RulesEngine/RulesEngine.cs | 20 +++-- src/RulesEngine/RulesEngine.csproj | 6 +- .../ActionTests/CustomActionTest.cs | 86 +++++++++++++++++++ .../ActionTests/MockClass/ReturnContextAction.cs | 28 +++++++ .../ActionTests/RulesEngineWithActionsTests.cs | 14 ++-- .../RulesEngine.UnitTest/BusinessRuleEngineTest.cs | 1 - test/RulesEngine.UnitTest/NestedRulesTest.cs | 97 ++++++++++++++++++++-- test/RulesEngine.UnitTest/RulesEnabledTests.cs | 4 +- .../RulesEngine.UnitTest.csproj | 3 +- 16 files changed, 304 insertions(+), 44 deletions(-) delete mode 100644 src/RulesEngine/Enums/ActionTriggerType.cs create mode 100644 src/RulesEngine/Models/RuleAction.cs create mode 100644 test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs create mode 100644 test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 58919c3..e83c06a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## [3.3.0] +- Added support for actions in nested rules +- Improved serialization support for System.Text.Json for workflow model + +Breaking Change: + - Type of Action has been changed from `Dictionary` to `RuleActions` + - No impact if you are serializing workflow from json + - For workflow objects created in code, refer - [link](https://github.com/microsoft/RulesEngine/pull/182/files#diff-a5093dda2dcc1e4958ce3533edb607bb61406e1f0a9071eca4e317bdd987c0d3) + ## [3.2.0] - Added AddOrUpdateWorkflow method to update workflows atomically (by @AshishPrasad) - Updated dependencies to latest diff --git a/RulesEngine.sln b/RulesEngine.sln index 85f2fd7..331f918 100644 --- a/RulesEngine.sln +++ b/RulesEngine.sln @@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution CHANGELOG.md = CHANGELOG.md global.json = global.json README.md = README.md + schema\workflowRules-schema.json = schema\workflowRules-schema.json EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RulesEngineBenchmark", "benchmark\RulesEngineBenchmark\RulesEngineBenchmark.csproj", "{C058809F-C720-4EFC-925D-A486627B238B}" diff --git a/schema/workflowRules-schema.json b/schema/workflowRules-schema.json index 84d39e0..333c25f 100644 --- a/schema/workflowRules-schema.json +++ b/schema/workflowRules-schema.json @@ -39,6 +39,12 @@ } ] } + }, + "Properties": { + "type": "object" + }, + "Actions": { + "$ref": "#/definitions/RuleActions" } }, "required": [ @@ -53,8 +59,7 @@ "type": "object", "required": [ "RuleName", - "Expression", - "RuleExpressionType" + "Expression" ], "properties": { "RuleName": { @@ -79,6 +84,35 @@ }, "SuccessEvent": { "type": "string" + }, + "Properties": { + "type": "object" + }, + "Actions": { + "$ref": "#/definitions/RuleActions" + } + } + }, + "ActionInfo": { + "propeties": { + "Name": { + "type": "string" + }, + "Context": { + "type": "object" + } + }, + "required": [ + "Name" + ] + }, + "RuleActions": { + "properties": { + "OnSuccess": { + "$ref": "#/definitions/ActionInfo" + }, + "OnFailure": { + "$ref": "#/definitions/ActionInfo" } } } diff --git a/src/RulesEngine/Actions/ActionContext.cs b/src/RulesEngine/Actions/ActionContext.cs index 4d0ba47..4f5bc13 100644 --- a/src/RulesEngine/Actions/ActionContext.cs +++ b/src/RulesEngine/Actions/ActionContext.cs @@ -19,7 +19,18 @@ namespace RulesEngine.Actions foreach (var kv in context) { string key = kv.Key; - string value = kv.Value is string ? kv.Value.ToString() : JsonConvert.SerializeObject(kv.Value); + string value; + switch (kv.Value.GetType().Name) + { + case "String": + case "JsonElement": + value = kv.Value.ToString(); + break; + default: + value = JsonConvert.SerializeObject(kv.Value); + break; + + } _context.Add(key, value); } _parentResult = parentResult; diff --git a/src/RulesEngine/Enums/ActionTriggerType.cs b/src/RulesEngine/Enums/ActionTriggerType.cs deleted file mode 100644 index 25b3ccb..0000000 --- a/src/RulesEngine/Enums/ActionTriggerType.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace RulesEngine.Enums -{ - public enum ActionTriggerType - { - onSuccess, - onFailure - } -} diff --git a/src/RulesEngine/Models/Rule.cs b/src/RulesEngine/Models/Rule.cs index 03010eb..611f890 100644 --- a/src/RulesEngine/Models/Rule.cs +++ b/src/RulesEngine/Models/Rule.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using RulesEngine.Enums; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -45,7 +44,7 @@ namespace RulesEngine.Models public IEnumerable Rules { get; set; } public IEnumerable LocalParams { get; set; } public string Expression { get; set; } - public Dictionary Actions { get; set; } + public RuleActions Actions { get; set; } public string SuccessEvent { get; set; } } diff --git a/src/RulesEngine/Models/RuleAction.cs b/src/RulesEngine/Models/RuleAction.cs new file mode 100644 index 0000000..cc5f50c --- /dev/null +++ b/src/RulesEngine/Models/RuleAction.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; + +namespace RulesEngine.Models +{ + [ExcludeFromCodeCoverage] + public class RuleActions + { + public ActionInfo OnSuccess { get; set; } + public ActionInfo OnFailure { get; set; } + } +} diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index b9d4304..29029e4 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -2,13 +2,11 @@ // Licensed under the MIT License. using FluentValidation; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using RulesEngine.Actions; -using RulesEngine.Enums; using RulesEngine.Exceptions; using RulesEngine.ExpressionBuilders; using RulesEngine.Interfaces; @@ -104,18 +102,26 @@ namespace RulesEngine public async ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams) { var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, ruleParams); + await ExecuteActionAsync(ruleResultList); + return ruleResultList; + } + + private async ValueTask ExecuteActionAsync(IEnumerable ruleResultList) + { foreach (var ruleResult in ruleResultList) { + if(ruleResult.ChildResults != null) + { + await ExecuteActionAsync(ruleResult.ChildResults); + } var actionResult = await ExecuteActionForRuleResult(ruleResult, false); ruleResult.ActionResult = new ActionResult { Output = actionResult.Output, Exception = actionResult.Exception }; } - return ruleResultList; } - public async ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters) { var compiledRule = CompileRule(workflowName, ruleName, ruleParameters); @@ -125,11 +131,11 @@ namespace RulesEngine private async ValueTask ExecuteActionForRuleResult(RuleResultTree resultTree, bool includeRuleResults = false) { - var triggerType = resultTree?.IsSuccess == true ? ActionTriggerType.onSuccess : ActionTriggerType.onFailure; + var ruleActions = resultTree?.Rule?.Actions; + var actionInfo = resultTree?.IsSuccess == true ? ruleActions?.OnSuccess : ruleActions?.OnFailure; - if (resultTree?.Rule?.Actions != null && resultTree.Rule.Actions.ContainsKey(triggerType)) + if (actionInfo != null) { - var actionInfo = resultTree.Rule.Actions[triggerType]; var action = _actionFactory.Get(actionInfo.Name); var ruleParameters = resultTree.Inputs.Select(kv => new RuleParameter(kv.Key, kv.Value)).ToArray(); return await action.ExecuteAndReturnResultAsync(new ActionContext(actionInfo.Context, resultTree), ruleParameters, includeRuleResults); diff --git a/src/RulesEngine/RulesEngine.csproj b/src/RulesEngine/RulesEngine.csproj index acb5be0..8692547 100644 --- a/src/RulesEngine/RulesEngine.csproj +++ b/src/RulesEngine/RulesEngine.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 3.2.0 + 3.3.0 Copyright (c) Microsoft Corporation. LICENSE https://github.com/microsoft/RulesEngine @@ -27,12 +27,12 @@ - + - + diff --git a/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs b/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs new file mode 100644 index 0000000..71f7ee3 --- /dev/null +++ b/test/RulesEngine.UnitTest/ActionTests/CustomActionTest.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using RulesEngine.Models; +using RulesEngine.UnitTest.ActionTests.MockClass; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Xunit; + +namespace RulesEngine.UnitTest.ActionTests +{ + [ExcludeFromCodeCoverage] + public class CustomActionTest + { + [Fact] + public async Task CustomActionOnRuleMustHaveContextValues() + { + var workflows = GetWorkflowRules(); + var re = new RulesEngine(workflows, null, reSettings: new ReSettings { + CustomActions = new Dictionary> { + + { "ReturnContext", () => new ReturnContextAction() } + } + }); + + var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true); + } + + + [Fact] + public async Task CustomAction_WithSystemTextJsobOnRuleMustHaveContextValues() + { + var workflows = GetWorkflowRules(); + var workflowStr = JsonConvert.SerializeObject(workflows); + var serializationOptions = new System.Text.Json.JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; + var workflowViaTextJson = System.Text.Json.JsonSerializer.Deserialize(workflowStr,serializationOptions); + + + var re = new RulesEngine(workflows, null, reSettings: new ReSettings { + CustomActions = new Dictionary> { + + { "ReturnContext", () => new ReturnContextAction() } + } + }); + + + + var result = await re.ExecuteAllRulesAsync("successReturnContextAction", true); + } + + private WorkflowRules[] GetWorkflowRules() + { + return new WorkflowRules[] { + new WorkflowRules { + WorkflowName = "successReturnContextAction", + Rules = new Rule[] { + new Rule { + RuleName = "trueRule", + Expression = "input1 == true", + Actions = new RuleActions() { + OnSuccess = new ActionInfo { + Name = "ReturnContext", + Context = new Dictionary { + {"stringContext", "hello"}, + {"intContext",1 }, + {"objectContext", new { a = "hello", b = 123 } } + } + } + + } + + }, + + + } + } + + }; + } + + + } +} diff --git a/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs b/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs new file mode 100644 index 0000000..174a849 --- /dev/null +++ b/test/RulesEngine.UnitTest/ActionTests/MockClass/ReturnContextAction.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using RulesEngine.Actions; +using RulesEngine.Models; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace RulesEngine.UnitTest.ActionTests.MockClass +{ + public class ReturnContextAction : ActionBase + { + public override ValueTask Run(ActionContext context, RuleParameter[] ruleParameters) + { + var stringContext = context.GetContext("stringContext"); + var intContext = context.GetContext("intContext"); + var objectContext = context.GetContext("objectContext"); + + return new ValueTask(new { + stringContext, + intContext, + objectContext + }); + } + } +} diff --git a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs index a35cf62..0d0d37d 100644 --- a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs +++ b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. - -using RulesEngine.Enums; using RulesEngine.Models; using System; using System.Collections.Generic; @@ -81,27 +79,27 @@ namespace RulesEngine.UnitTest RuleName = "ExpressionOutputRuleTest", RuleExpressionType = RuleExpressionType.LambdaExpression, Expression = "1 == 1", - Actions = new Dictionary{ - { ActionTriggerType.onSuccess, new ActionInfo{ + Actions = new RuleActions{ + OnSuccess = new ActionInfo{ Name = "OutputExpression", Context = new Dictionary{ {"expression", "2*2"} } - }} + } } }, new Rule{ RuleName = "EvaluateRuleTest", RuleExpressionType = RuleExpressionType.LambdaExpression, Expression = "1 == 1", - Actions = new Dictionary{ - { ActionTriggerType.onSuccess, new ActionInfo{ + Actions = new RuleActions{ + OnSuccess = new ActionInfo{ Name = "EvaluateRule", Context = new Dictionary{ {"workflowName", "ActionWorkflow"}, {"ruleName","ExpressionOutputRuleTest"} } - }} + } } } diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index d477464..0d1e01f 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -712,7 +712,6 @@ namespace RulesEngine.UnitTest var workflowStr = "{\"WorkflowName\":\"Exámple\",\"WorkflowRulesToInject\":null,\"GlobalParams\":null,\"Rules\":[{\"RuleName\":\"RuleWithLocalParam\",\"Properties\":null,\"Operator\":null,\"ErrorMessage\":null,\"Enabled\":true,\"ErrorType\":\"Warning\",\"RuleExpressionType\":\"LambdaExpression\",\"WorkflowRulesToInject\":null,\"Rules\":null,\"LocalParams\":null,\"Expression\":\"input1 == null || input1.hello.world = \\\"wow\\\"\",\"Actions\":null,\"SuccessEvent\":null}]}"; var re = new RulesEngine(new string[] { workflowStr },null,null); - // re.AddWorkflow(workflowStr); dynamic input1 = new ExpandoObject(); input1.hello = new ExpandoObject(); diff --git a/test/RulesEngine.UnitTest/NestedRulesTest.cs b/test/RulesEngine.UnitTest/NestedRulesTest.cs index 99c049e..b11da4b 100644 --- a/test/RulesEngine.UnitTest/NestedRulesTest.cs +++ b/test/RulesEngine.UnitTest/NestedRulesTest.cs @@ -1,11 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Newtonsoft.Json; using RulesEngine.Models; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Dynamic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Xunit; @@ -21,8 +23,8 @@ namespace RulesEngine.UnitTest public async Task NestedRulesShouldFollowExecutionMode(NestedRuleExecutionMode mode) { var workflows = GetWorkflows(); - var reSettings = new ReSettings { NestedRuleExecutionMode = mode}; - var rulesEngine = new RulesEngine(workflows, reSettings:reSettings); + var reSettings = new ReSettings { NestedRuleExecutionMode = mode }; + var rulesEngine = new RulesEngine(workflows, reSettings: reSettings); dynamic input1 = new ExpandoObject(); input1.trueValue = true; @@ -32,10 +34,10 @@ namespace RulesEngine.UnitTest Assert.All(andResults, c => Assert.False(c.IsSuccess) ); - Assert.All(orResults, + Assert.All(orResults, c => Assert.True(c.IsSuccess)); - if(mode == NestedRuleExecutionMode.All) + if (mode == NestedRuleExecutionMode.All) { Assert.All(andResults, c => Assert.Equal(c.Rule.Rules.Count(), c.ChildResults.Count())); @@ -60,9 +62,49 @@ namespace RulesEngine.UnitTest } - + } + [Fact] + private async Task NestedRulesWithNestedActions_ReturnsCorrectResults() + { + var workflows = GetWorkflows(); + var reSettings = new ReSettings { }; + var rulesEngine = new RulesEngine(workflows, reSettings: reSettings); + dynamic input1 = new ExpandoObject(); + input1.trueValue = true; + + List result = await rulesEngine.ExecuteAllRulesAsync("NestedRulesActionsTest", input1); + + Assert.False(result[0].IsSuccess); + Assert.Equal(input1.trueValue, result[0].ActionResult.Output); + Assert.All(result[0].ChildResults, (childResult) => Assert.Equal(input1.trueValue, childResult.ActionResult.Output)); + } + + [Fact] + private async Task NestedRulesWithNestedActions_WorkflowParsedWithSystemTextJson_ReturnsCorrectResults() + { + var workflows = GetWorkflows(); + var workflowStr = JsonConvert.SerializeObject(workflows); + + var serializationOptions = new System.Text.Json.JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; + + + var workflowsViaTextJson = System.Text.Json.JsonSerializer.Deserialize(workflowStr, serializationOptions); + + var reSettings = new ReSettings { }; + var rulesEngine = new RulesEngine(workflowsViaTextJson, reSettings: reSettings); + dynamic input1 = new ExpandoObject(); + input1.trueValue = true; + + List result = await rulesEngine.ExecuteAllRulesAsync("NestedRulesActionsTest", input1); + + Assert.False(result[0].IsSuccess); + Assert.Equal(input1.trueValue, result[0].ActionResult.Output); + Assert.All(result[0].ChildResults, (childResult) => Assert.Equal(input1.trueValue, childResult.ActionResult.Output)); + + + } @@ -134,7 +176,50 @@ namespace RulesEngine.UnitTest } } }, - + new WorkflowRules { + WorkflowName = "NestedRulesActionsTest", + Rules = new Rule[] { + new Rule { + RuleName = "AndRuleTrueFalse", + Operator = "And", + Rules = new Rule[] { + new Rule{ + RuleName = "trueRule1", + Expression = "input1.TrueValue == true", + Actions = new RuleActions { + OnSuccess = new ActionInfo{ + Name = "OutputExpression", + Context = new Dictionary { + { "Expression", "input1.TrueValue" } + } + } + } + }, + new Rule { + RuleName = "falseRule1", + Expression = "input1.TrueValue == false", + Actions = new RuleActions { + OnFailure = new ActionInfo{ + Name = "OutputExpression", + Context = new Dictionary { + { "Expression", "input1.TrueValue" } + } + } + } + } + }, + Actions = new RuleActions { + OnFailure = new ActionInfo{ + Name = "OutputExpression", + Context = new Dictionary { + { "Expression", "input1.TrueValue" } + } + } + } + } + } + } + }; } } diff --git a/test/RulesEngine.UnitTest/RulesEnabledTests.cs b/test/RulesEngine.UnitTest/RulesEnabledTests.cs index 1b865ea..8ddc992 100644 --- a/test/RulesEngine.UnitTest/RulesEnabledTests.cs +++ b/test/RulesEngine.UnitTest/RulesEnabledTests.cs @@ -23,7 +23,7 @@ namespace RulesEngine.UnitTest public async Task RulesEngine_ShouldOnlyExecuteEnabledRules(string workflowName, bool[] expectedRuleResults) { var workflows = GetWorkflows(); - var rulesEngine = new RulesEngine(workflows); + var rulesEngine = new RulesEngine(workflows, reSettings: new ReSettings() { EnableExceptionAsErrorMessage = false }); var input1 = new { TrueValue = true }; @@ -45,7 +45,7 @@ namespace RulesEngine.UnitTest public async Task WorkflowUpdatedRuleEnabled_ShouldReflect(string workflowName, bool[] expectedRuleResults) { var workflow = GetWorkflows().Single(c => c.WorkflowName == workflowName); - var rulesEngine = new RulesEngine(); + var rulesEngine = new RulesEngine(reSettings: new ReSettings() { EnableExceptionAsErrorMessage = false}); rulesEngine.AddWorkflow(workflow); var input1 = new { TrueValue = true diff --git a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj index 7bc8325..db13539 100644 --- a/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj +++ b/test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj @@ -6,12 +6,13 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive