From 2a02a42a59030702400a57cd03201195b584841c Mon Sep 17 00:00:00 2001 From: Jake Moresca Date: Wed, 15 Nov 2023 17:56:49 +0100 Subject: [PATCH] initial commit --- ActionFlow.Tests/ActionFlow.Tests.csproj | 24 ++++ .../Engine/RuleEngineWrapperTests.cs | 56 +++++++++ ActionFlow.Tests/Engine/WorkflowTests.cs | 45 +++++++ ActionFlow.Tests/Rules/VariableRuleTests.cs | 110 ++++++++++++++++++ ActionFlow.Tests/Usings.cs | 1 + ActionFlow.sln | 31 +++++ ActionFlow/ActionFlow.csproj | 13 +++ ActionFlow/ActionFlow.sln | 25 ++++ ActionFlow/Engine/IRuleEngineWrapper.cs | 9 ++ ActionFlow/Engine/IWorkflowProvider.cs | 9 ++ ActionFlow/Engine/RuleEngineWrapper.cs | 20 ++++ ActionFlow/Engine/WorkflowProvider.cs | 33 ++++++ ActionFlow/Rules/IRuleWrapper.cs | 16 +++ ActionFlow/Rules/VariableRule.cs | 52 +++++++++ 14 files changed, 444 insertions(+) create mode 100644 ActionFlow.Tests/ActionFlow.Tests.csproj create mode 100644 ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs create mode 100644 ActionFlow.Tests/Engine/WorkflowTests.cs create mode 100644 ActionFlow.Tests/Rules/VariableRuleTests.cs create mode 100644 ActionFlow.Tests/Usings.cs create mode 100644 ActionFlow.sln create mode 100644 ActionFlow/ActionFlow.csproj create mode 100644 ActionFlow/ActionFlow.sln create mode 100644 ActionFlow/Engine/IRuleEngineWrapper.cs create mode 100644 ActionFlow/Engine/IWorkflowProvider.cs create mode 100644 ActionFlow/Engine/RuleEngineWrapper.cs create mode 100644 ActionFlow/Engine/WorkflowProvider.cs create mode 100644 ActionFlow/Rules/IRuleWrapper.cs create mode 100644 ActionFlow/Rules/VariableRule.cs diff --git a/ActionFlow.Tests/ActionFlow.Tests.csproj b/ActionFlow.Tests/ActionFlow.Tests.csproj new file mode 100644 index 0000000..3100047 --- /dev/null +++ b/ActionFlow.Tests/ActionFlow.Tests.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + diff --git a/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs b/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs new file mode 100644 index 0000000..94d65f5 --- /dev/null +++ b/ActionFlow.Tests/Engine/RuleEngineWrapperTests.cs @@ -0,0 +1,56 @@ +using ActionFlow.Engine; +using NSubstitute; +using RulesEngine.Models; +using System.Dynamic; + +namespace ActionFlow.Tests.Engine +{ + [TestClass] + public class RuleEngineWrapperTests + { + [TestMethod] + public void When_running_rule_engine_with_basic_workflow_it_should_execute() + { + //Arrange + var workflowProvider = Substitute.For(); + workflowProvider.GetAllWorkflows().Returns(CreateFakeWorkflows()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider); + + //Act + dynamic datas = new ExpandoObject(); + datas.count = 1; + var inputs = new dynamic[] + { + datas + }; + + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", inputs).Result; + + //Assert + Assert.IsTrue(resultList.TrueForAll(x => x.IsSuccess)); + } + + private List CreateFakeWorkflows() + { + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + Rule rule = new Rule(); + rule.RuleName = "Test Rule"; + rule.SuccessEvent = "Count is within tolerance."; + rule.ErrorMessage = "Over expected."; + rule.Expression = "count < 3"; + rule.RuleExpressionType = RuleExpressionType.LambdaExpression; + + rules.Add(rule); + workflow.Rules = rules; + workflows.Add(workflow); + + return workflows; + } + } +} diff --git a/ActionFlow.Tests/Engine/WorkflowTests.cs b/ActionFlow.Tests/Engine/WorkflowTests.cs new file mode 100644 index 0000000..6ff941a --- /dev/null +++ b/ActionFlow.Tests/Engine/WorkflowTests.cs @@ -0,0 +1,45 @@ +using RulesEngine.Models; +using System.Dynamic; + +namespace ActionFlow.Tests.Engine +{ + [TestClass] + public class WorkflowTests + { + [TestMethod] + public void When_running_basic_workflow_it_should_execute() + { + //Arrange + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + Rule rule = new Rule(); + rule.RuleName = "Test Rule"; + rule.SuccessEvent = "Count is within tolerance."; + rule.ErrorMessage = "Over expected."; + rule.Expression = "count < 3"; + rule.RuleExpressionType = RuleExpressionType.LambdaExpression; + + rules.Add(rule); + workflow.Rules = rules; + workflows.Add(workflow); + var rulesEngine = new RulesEngine.RulesEngine(workflows.ToArray(), null); + + dynamic datas = new ExpandoObject(); + datas.count = 1; + var inputs = new dynamic[] + { + datas + }; + + //Act + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", inputs).Result; + + //Assert + Assert.IsTrue(resultList.TrueForAll(x => x.IsSuccess)); + } + } +} \ No newline at end of file diff --git a/ActionFlow.Tests/Rules/VariableRuleTests.cs b/ActionFlow.Tests/Rules/VariableRuleTests.cs new file mode 100644 index 0000000..86f596e --- /dev/null +++ b/ActionFlow.Tests/Rules/VariableRuleTests.cs @@ -0,0 +1,110 @@ +using ActionFlow.Engine; +using ActionFlow.Rules; +using NSubstitute; +using RulesEngine.Models; + +namespace ActionFlow.Tests.Rules +{ + [TestClass] + public class VariableRuleTests + { + + [TestMethod] + public void When_running_rule_engine_with_basic_workflow_it_should_execute() + { + //Arrange + var workflowProvider = Substitute.For(); + workflowProvider.GetAllWorkflows().Returns(CreateBasicWorkflow()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider); + + //Act + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; + + //Assert + Assert.IsTrue(resultList.TrueForAll(x => x.IsSuccess)); + Assert.AreEqual(2, resultList[0].Rule.LocalParams.Count()); + } + + [TestMethod] + public void When_running_rule_engine_with_false_condition_on_variable_rule_child_it_should_fail() + { + //Arrange + var workflowProvider = Substitute.For(); + workflowProvider.GetAllWorkflows().Returns(CreateConditionExpressionWorkflow()); + + var rulesEngine = new RuleEngineWrapper(workflowProvider); + + //Act + var resultList = rulesEngine.ExecuteAllRulesAsync("Test Workflow Rule 1", new object[] { }).Result; + + //Assert + Assert.IsFalse(resultList.TrueForAll(x => x.IsSuccess)); + Assert.AreEqual(2, resultList[0].Rule.LocalParams.Count()); + } + + private List CreateBasicWorkflow() + { + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + var variableRule1 = new VariableRule(); + variableRule1.Name = "Declare test variables"; + variableRule1.Variables = new Dictionary + { + { "age", "1" }, + { "canWalk", "false" } + }; + variableRule1.ConditionExpression = "age == 1 && canWalk == false"; + + var variableRule2 = new VariableRule(); + variableRule2.Name = "Test previously declared variables"; + variableRule2.ConditionExpression = "age == 1 && canWalk == false"; + + var variableRule3 = new VariableRule(); + variableRule3.Name = "Test previously declared variables 2"; + variableRule3.ConditionExpression = "age == 1 && canWalk == false"; + + variableRule1.Rules.Add(variableRule2); + variableRule1.Rules.Add(variableRule3); + + rules.Add(variableRule1.AsRuleEngineRule()); + workflow.Rules = rules; + workflows.Add(workflow); + + return workflows; + } + + private List CreateConditionExpressionWorkflow() + { + List workflows = new List(); + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + var variableRule1 = new VariableRule(); + variableRule1.Name = "Declare test variables"; + variableRule1.Variables = new Dictionary + { + { "age", "1" }, + { "canWalk", "false" } + }; + + var variableRule2 = new VariableRule(); + variableRule2.Name = "Test previously declared variables"; + variableRule2.ConditionExpression = "age == 1 && canWalk == true"; + + variableRule1.Rules.Add(variableRule2); + + rules.Add(variableRule1.AsRuleEngineRule()); + workflow.Rules = rules; + workflows.Add(workflow); + + return workflows; + } + } +} diff --git a/ActionFlow.Tests/Usings.cs b/ActionFlow.Tests/Usings.cs new file mode 100644 index 0000000..ab67c7e --- /dev/null +++ b/ActionFlow.Tests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/ActionFlow.sln b/ActionFlow.sln new file mode 100644 index 0000000..fd9d60b --- /dev/null +++ b/ActionFlow.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActionFlow.Tests", "ActionFlow.Tests\ActionFlow.Tests.csproj", "{AACB88C1-1FA4-4D10-B6DD-F5F1CEDAFD62}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ActionFlow", "ActionFlow\ActionFlow.csproj", "{EFBB5884-ED06-4DC3-8324-3C14C7991ECB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AACB88C1-1FA4-4D10-B6DD-F5F1CEDAFD62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AACB88C1-1FA4-4D10-B6DD-F5F1CEDAFD62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AACB88C1-1FA4-4D10-B6DD-F5F1CEDAFD62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AACB88C1-1FA4-4D10-B6DD-F5F1CEDAFD62}.Release|Any CPU.Build.0 = Release|Any CPU + {EFBB5884-ED06-4DC3-8324-3C14C7991ECB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFBB5884-ED06-4DC3-8324-3C14C7991ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFBB5884-ED06-4DC3-8324-3C14C7991ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFBB5884-ED06-4DC3-8324-3C14C7991ECB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {157E2014-67B6-43AA-85D5-304BD88DE941} + EndGlobalSection +EndGlobal diff --git a/ActionFlow/ActionFlow.csproj b/ActionFlow/ActionFlow.csproj new file mode 100644 index 0000000..21a2525 --- /dev/null +++ b/ActionFlow/ActionFlow.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/ActionFlow/ActionFlow.sln b/ActionFlow/ActionFlow.sln new file mode 100644 index 0000000..858e5a5 --- /dev/null +++ b/ActionFlow/ActionFlow.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33829.357 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ActionFlow", "ActionFlow.csproj", "{8EB94BDF-60AA-4581-8486-F6A3AC25DD40}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8EB94BDF-60AA-4581-8486-F6A3AC25DD40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EB94BDF-60AA-4581-8486-F6A3AC25DD40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EB94BDF-60AA-4581-8486-F6A3AC25DD40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EB94BDF-60AA-4581-8486-F6A3AC25DD40}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {157E2014-67B6-43AA-85D5-304BD88DE941} + EndGlobalSection +EndGlobal diff --git a/ActionFlow/Engine/IRuleEngineWrapper.cs b/ActionFlow/Engine/IRuleEngineWrapper.cs new file mode 100644 index 0000000..eff0b47 --- /dev/null +++ b/ActionFlow/Engine/IRuleEngineWrapper.cs @@ -0,0 +1,9 @@ +using RulesEngine.Models; + +namespace ActionFlow.Engine +{ + public interface IRuleEngineWrapper + { + ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs); + } +} \ No newline at end of file diff --git a/ActionFlow/Engine/IWorkflowProvider.cs b/ActionFlow/Engine/IWorkflowProvider.cs new file mode 100644 index 0000000..591874a --- /dev/null +++ b/ActionFlow/Engine/IWorkflowProvider.cs @@ -0,0 +1,9 @@ +using RulesEngine.Models; + +namespace ActionFlow.Engine +{ + public interface IWorkflowProvider + { + List GetAllWorkflows(); + } +} \ No newline at end of file diff --git a/ActionFlow/Engine/RuleEngineWrapper.cs b/ActionFlow/Engine/RuleEngineWrapper.cs new file mode 100644 index 0000000..fe8eced --- /dev/null +++ b/ActionFlow/Engine/RuleEngineWrapper.cs @@ -0,0 +1,20 @@ +using RulesEngine.Interfaces; +using RulesEngine.Models; + +namespace ActionFlow.Engine +{ + public class RuleEngineWrapper : IRuleEngineWrapper + { + private readonly IRulesEngine _rulesEngine; + + public RuleEngineWrapper(IWorkflowProvider workflowProvider) + { + _rulesEngine = new RulesEngine.RulesEngine(workflowProvider.GetAllWorkflows().ToArray(), null); + } + + public ValueTask> ExecuteAllRulesAsync(string workflowName, params object[] inputs) + { + return _rulesEngine.ExecuteAllRulesAsync(workflowName, inputs); + } + } +} diff --git a/ActionFlow/Engine/WorkflowProvider.cs b/ActionFlow/Engine/WorkflowProvider.cs new file mode 100644 index 0000000..aba0cb0 --- /dev/null +++ b/ActionFlow/Engine/WorkflowProvider.cs @@ -0,0 +1,33 @@ +using RulesEngine.Models; + +namespace ActionFlow.Engine +{ + public class WorkflowProvider : IWorkflowProvider + { + public List GetAllWorkflows() + { + List workflows = new List(); + + //Test + //Todo: Load from DB, Json, or other source + Workflow workflow = new Workflow(); + workflow.WorkflowName = "Test Workflow Rule 1"; + + List rules = new List(); + + Rule rule = new Rule(); + rule.RuleName = "Test Rule"; + rule.SuccessEvent = "Count is within tolerance."; + rule.ErrorMessage = "Over expected."; + rule.Expression = "count < 3"; + rule.RuleExpressionType = RuleExpressionType.LambdaExpression; + + rules.Add(rule); + workflow.Rules = rules; + workflows.Add(workflow); + //Test End + + return workflows; + } + } +} diff --git a/ActionFlow/Rules/IRuleWrapper.cs b/ActionFlow/Rules/IRuleWrapper.cs new file mode 100644 index 0000000..2a16149 --- /dev/null +++ b/ActionFlow/Rules/IRuleWrapper.cs @@ -0,0 +1,16 @@ +using RulesEngine.Models; + +namespace ActionFlow.Rules +{ + public interface IRuleWrapper + { + string ConditionExpression { get; set; } + string ErrorMessage { get; set; } + string Name { get; set; } + string RuleType { get; } + string SuccessEvent { get; set; } + Dictionary Variables { get; set; } + + Rule AsRuleEngineRule(); + } +} \ No newline at end of file diff --git a/ActionFlow/Rules/VariableRule.cs b/ActionFlow/Rules/VariableRule.cs new file mode 100644 index 0000000..3756314 --- /dev/null +++ b/ActionFlow/Rules/VariableRule.cs @@ -0,0 +1,52 @@ +using RulesEngine.Models; + +namespace ActionFlow.Rules +{ + /// + /// Variable Rule is a kind of rule for storing variables which stored values can be used by child rules + /// + public class VariableRule : IRuleWrapper + { + public string RuleType => "Variable"; + public string Name { get; set; } = string.Empty; + public string SuccessEvent { get; set; } = string.Empty; + public string ErrorMessage { get; set; } = string.Empty; + public string ConditionExpression { get; set; } = string.Empty; + public Dictionary Variables { get; set; } = new Dictionary(); + public List Rules { get; set; } = new List { }; + + public Rule AsRuleEngineRule() + { + Rule rule = new Rule(); + + rule.RuleName = $"{RuleType} - {Name}"; + rule.SuccessEvent = SuccessEvent; + rule.ErrorMessage = ErrorMessage; + + rule.LocalParams = Variables.Select(x => + { + return new ScopedParam { Name = x.Key, Expression = x.Value }; + }); + + if (ConditionExpression != string.Empty) + { + rule.Expression = ConditionExpression; + rule.RuleExpressionType = RuleExpressionType.LambdaExpression; + } + + //if (!rule.LocalParams.Any()) + //{ + // rule.Expression = ConditionExpression; + //} + + rule.Rules = Rules.Select(x => x.AsRuleEngineRule()).ToList(); + + if (rule.Rules.Any()) + { + rule.Operator = "And"; + } + + return rule; + } + } +}