From 97c3f24daa9de399986e87f10c52034eacd78188 Mon Sep 17 00:00:00 2001 From: Abbas Cyclewala Date: Sat, 20 Nov 2021 12:08:42 +0530 Subject: [PATCH 1/2] * fixed workflowInjection not working * added optional Inputs filter for EvaluateRuleAction --- .../RulesEngineContext.cs | 1 - src/RulesEngine/Actions/ActionContext.cs | 15 ++++ src/RulesEngine/Actions/EvaluateRuleAction.cs | 14 +++- src/RulesEngine/Models/Workflow.cs | 1 - src/RulesEngine/RulesCache.cs | 2 +- .../RulesEngineWithActionsTests.cs | 72 ++++++++++++++++++- .../BusinessRuleEngineTest.cs | 6 +- 7 files changed, 101 insertions(+), 10 deletions(-) diff --git a/demo/DemoApp.EFDataExample/RulesEngineContext.cs b/demo/DemoApp.EFDataExample/RulesEngineContext.cs index 6cf4afd8..b3e0c0d0 100644 --- a/demo/DemoApp.EFDataExample/RulesEngineContext.cs +++ b/demo/DemoApp.EFDataExample/RulesEngineContext.cs @@ -21,7 +21,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(entity => { entity.HasKey(k => k.WorkflowName); - entity.Ignore(b => b.WorkflowRulesToInject); entity.Ignore(b => b.WorkflowsToInject); }); diff --git a/src/RulesEngine/Actions/ActionContext.cs b/src/RulesEngine/Actions/ActionContext.cs index 4f5bc130..af66cdc8 100644 --- a/src/RulesEngine/Actions/ActionContext.cs +++ b/src/RulesEngine/Actions/ActionContext.cs @@ -40,6 +40,21 @@ public RuleResultTree GetParentRuleResult() { return _parentResult; } + + public bool TryGetContext(string name,out T output) + { + try + { + output = GetContext(name); + return true; + } + catch(ArgumentException) + { + output = default(T); + return false; + } + } + public T GetContext(string name) { try diff --git a/src/RulesEngine/Actions/EvaluateRuleAction.cs b/src/RulesEngine/Actions/EvaluateRuleAction.cs index a93f7475..613859cd 100644 --- a/src/RulesEngine/Actions/EvaluateRuleAction.cs +++ b/src/RulesEngine/Actions/EvaluateRuleAction.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using RulesEngine.Models; +using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace RulesEngine.Actions @@ -23,11 +25,11 @@ internal async override ValueTask ExecuteAndReturnResultAsync( List resultList = null; if (includeRuleResults) { - resultList = new List(output.Results); + resultList = new List(output?.Results ?? new List() { }); resultList.AddRange(innerResult.Results); } return new ActionRuleResult { - Output = output.Output, + Output = output?.Output, Exception = innerResult.Exception, Results = resultList }; @@ -37,7 +39,13 @@ public async override ValueTask Run(ActionContext context, RuleParameter { var workflowName = context.GetContext("workflowName"); var ruleName = context.GetContext("ruleName"); - var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, ruleParameters); + var filteredRuleParameters = ruleParameters; + if(context.TryGetContext>("inputFilter",out var inputFilter)) + { + filteredRuleParameters = ruleParameters.Where(c => inputFilter.Contains(c.Name)).ToArray(); + } + + var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, filteredRuleParameters); return ruleResult; } } diff --git a/src/RulesEngine/Models/Workflow.cs b/src/RulesEngine/Models/Workflow.cs index 79e163e5..7e388752 100644 --- a/src/RulesEngine/Models/Workflow.cs +++ b/src/RulesEngine/Models/Workflow.cs @@ -27,7 +27,6 @@ public class Workflow /// The workflow rules to inject. [Obsolete("WorkflowRulesToInject is deprecated. Use WorkflowsToInject instead.")] public IEnumerable WorkflowRulesToInject { - get { return WorkflowsToInject; } set { WorkflowsToInject = value; } } public IEnumerable WorkflowsToInject { get; set; } diff --git a/src/RulesEngine/RulesCache.cs b/src/RulesEngine/RulesCache.cs index 256fcf36..0d17d3e6 100644 --- a/src/RulesEngine/RulesCache.cs +++ b/src/RulesEngine/RulesCache.cs @@ -107,7 +107,7 @@ public Workflow GetWorkflow(string workflowName) throw new Exception($"Could not find injected Workflow: {wfname}"); } - workflow.Rules.ToList().AddRange(injectedWorkflow.Rules); + workflow.Rules = workflow.Rules.Concat(injectedWorkflow.Rules).ToList(); } } diff --git a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs index 9fa03742..af900c51 100644 --- a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs +++ b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs @@ -53,6 +53,17 @@ public async Task ExecuteActionWorkflowAsync_CalledWithNoActionsInWorkflow_Execu } + [Fact] + public async Task ExecuteActionWorkflowAsync_SelfReferencingAction_NoFilter_ExecutesSuccessfully() + { + + var engine = new RulesEngine(GetWorkflowWithActions()); + var result = await engine.ExecuteActionWorkflowAsync("WorkflowWithGlobalsAndSelfRefActions", "RuleReferencingSameWorkflow", new RuleParameter[0]); + Assert.NotNull(result); + Assert.Null(result.Output); + } + + private Workflow[] GetWorkflowsWithoutActions() { var workflow1 = new Workflow { @@ -104,8 +115,67 @@ private Workflow[] GetWorkflowWithActions() } } + + }; - return new[] { workflow1 }; + + var workflow2 = new Workflow { + WorkflowName = "WorkflowWithGlobalsAndSelfRefActions", + GlobalParams = new[] { + new ScopedParam { + Name = "global1", + Expression = "\"Hello\"" + } + }, + Rules = new[] { + + new Rule{ + RuleName = "RuleReferencingSameWorkflow", + Expression = "1 == 1", + Actions = new RuleActions { + OnSuccess = new ActionInfo{ + Name = "EvaluateRule", + Context = new Dictionary{ + {"workflowName", "WorkflowWithGlobalsAndSelfRefActions"}, + {"ruleName","OtherRule"} + } + } + } + },new Rule{ + RuleName = "RuleReferencingSameWorkflowWithInputFilter", + Expression = "1 == 1", + Actions = new RuleActions { + OnSuccess = new ActionInfo{ + Name = "EvaluateRule", + Context = new Dictionary{ + {"workflowName", "WorkflowWithGlobalsAndSelfRefActions"}, + {"ruleName","OtherRule"}, + {"inputFilter",new string[] { } } + } + + } + } + } + + + , new Rule{ + RuleName = "OtherRule", + Expression = "1 == 1", + Actions = new RuleActions { + OnSuccess = new ActionInfo{ + Name = "OutputExpression", + Context = new Dictionary{ + {"expression", "2*2"} + } + } + } + + } + + } + + }; + return new[] { workflow1, workflow2 }; } } } \ No newline at end of file diff --git a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs index 8bef3bf0..0cd9f104 100644 --- a/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs +++ b/test/RulesEngine.UnitTest/BusinessRuleEngineTest.cs @@ -35,7 +35,7 @@ public void RulesEngine_New_ReturnsNotNull(string ruleFileName) [Theory] [InlineData("rules2.json")] - public async Task RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string ruleFileName) + public async Task RulesEngine_InjectedRules_ContainsInjectedRules(string ruleFileName) { var re = GetRulesEngine(ruleFileName); @@ -43,9 +43,9 @@ public async Task RulesEngine_InjectedRules_ReturnsListOfRuleResultTree(string r dynamic input2 = GetInput2(); dynamic input3 = GetInput3(); - var result = await re.ExecuteAllRulesAsync("inputWorkflowReference", input1, input2, input3); + List result = await re.ExecuteAllRulesAsync("inputWorkflowReference", input1, input2, input3); Assert.NotNull(result); - Assert.IsType>(result); + Assert.True(result.Any()); } [Theory] From 574e44a294183131cb1369c149188ea70b006f7a Mon Sep 17 00:00:00 2001 From: Abbas Cyclewala Date: Sat, 20 Nov 2021 21:21:09 +0530 Subject: [PATCH 2/2] * ActionContext now supports optional inputs * Added support to pass additionalInputs to EvaluateRule Action --- src/RulesEngine/Actions/EvaluateRuleAction.cs | 22 ++++++++++++++----- src/RulesEngine/RulesEngine.cs | 2 +- .../RulesEngineWithActionsTests.cs | 20 +++++++++++++++-- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/RulesEngine/Actions/EvaluateRuleAction.cs b/src/RulesEngine/Actions/EvaluateRuleAction.cs index 613859cd..032308e8 100644 --- a/src/RulesEngine/Actions/EvaluateRuleAction.cs +++ b/src/RulesEngine/Actions/EvaluateRuleAction.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using RulesEngine.ExpressionBuilders; using RulesEngine.Models; using System; using System.Collections.Generic; @@ -12,10 +13,12 @@ namespace RulesEngine.Actions public class EvaluateRuleAction : ActionBase { private readonly RulesEngine _ruleEngine; + private readonly RuleExpressionParser _ruleExpressionParser; - public EvaluateRuleAction(RulesEngine ruleEngine) + public EvaluateRuleAction(RulesEngine ruleEngine, RuleExpressionParser ruleExpressionParser) { _ruleEngine = ruleEngine; + _ruleExpressionParser = ruleExpressionParser; } internal async override ValueTask ExecuteAndReturnResultAsync(ActionContext context, RuleParameter[] ruleParameters, bool includeRuleResults = false) @@ -39,13 +42,22 @@ public async override ValueTask Run(ActionContext context, RuleParameter { var workflowName = context.GetContext("workflowName"); var ruleName = context.GetContext("ruleName"); - var filteredRuleParameters = ruleParameters; + var filteredRuleParameters = new List(ruleParameters); if(context.TryGetContext>("inputFilter",out var inputFilter)) { - filteredRuleParameters = ruleParameters.Where(c => inputFilter.Contains(c.Name)).ToArray(); + filteredRuleParameters = ruleParameters.Where(c => inputFilter.Contains(c.Name)).ToList(); } - - var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, filteredRuleParameters); + if (context.TryGetContext>("additionalInputs", out var additionalInputs)) + { + foreach(var additionalInput in additionalInputs) + { + dynamic value = _ruleExpressionParser.Evaluate(additionalInput.Expression, ruleParameters); + filteredRuleParameters.Add(new RuleParameter(additionalInput.Name, value)); + + } + } + + var ruleResult = await _ruleEngine.ExecuteActionWorkflowAsync(workflowName, ruleName, filteredRuleParameters.ToArray()); return ruleResult; } } diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs index f0dabdc0..beac63aa 100644 --- a/src/RulesEngine/RulesEngine.cs +++ b/src/RulesEngine/RulesEngine.cs @@ -345,7 +345,7 @@ private IDictionary> GetDefaultActionRegistry() { return new Dictionary>{ {"OutputExpression",() => new OutputExpressionAction(_ruleExpressionParser) }, - {"EvaluateRule", () => new EvaluateRuleAction(this) } + {"EvaluateRule", () => new EvaluateRuleAction(this,_ruleExpressionParser) } }; } diff --git a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs index af900c51..942959e7 100644 --- a/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs +++ b/test/RulesEngine.UnitTest/ActionTests/RulesEngineWithActionsTests.cs @@ -63,6 +63,15 @@ public async Task ExecuteActionWorkflowAsync_SelfReferencingAction_NoFilter_Exec Assert.Null(result.Output); } + [Fact] + public async Task ExecuteActionWorkflowAsync_SelfReferencingAction_WithFilter_ExecutesSuccessfully() + { + + var engine = new RulesEngine(GetWorkflowWithActions()); + var result = await engine.ExecuteActionWorkflowAsync("WorkflowWithGlobalsAndSelfRefActions", "RuleReferencingSameWorkflowWithInputFilter", new RuleParameter[0]); + Assert.NotNull(result); + Assert.Equal(4,result.Output); + } private Workflow[] GetWorkflowsWithoutActions() { @@ -150,7 +159,14 @@ private Workflow[] GetWorkflowWithActions() Context = new Dictionary{ {"workflowName", "WorkflowWithGlobalsAndSelfRefActions"}, {"ruleName","OtherRule"}, - {"inputFilter",new string[] { } } + {"inputFilter",new string[] { } }, + {"additionalInputs", new [] { + new ScopedParam(){ + Name = "additionalValue", + Expression = "1" + } + + } } } } @@ -160,7 +176,7 @@ private Workflow[] GetWorkflowWithActions() , new Rule{ RuleName = "OtherRule", - Expression = "1 == 1", + Expression = "additionalValue == 1", Actions = new RuleActions { OnSuccess = new ActionInfo{ Name = "OutputExpression",