Abbasc52/actions for nested levels (#182)

* Added support for nested rule actions

* Changed type for Actions
pull/147/head v3.3.0
Abbas Cyclewala 2021-07-20 16:54:32 +05:30 committed by GitHub
parent bafbff281d
commit b763f718bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 304 additions and 44 deletions

View File

@ -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<ActionTriggerType, ActionInfo>` 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

View File

@ -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}"

View File

@ -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"
}
}
}

View File

@ -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;

View File

@ -1,11 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
namespace RulesEngine.Enums
{
public enum ActionTriggerType
{
onSuccess,
onFailure
}
}

View File

@ -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<Rule> Rules { get; set; }
public IEnumerable<ScopedParam> LocalParams { get; set; }
public string Expression { get; set; }
public Dictionary<ActionTriggerType, ActionInfo> Actions { get; set; }
public RuleActions Actions { get; set; }
public string SuccessEvent { get; set; }
}

View File

@ -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; }
}
}

View File

@ -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<List<RuleResultTree>> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams)
{
var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, ruleParams);
await ExecuteActionAsync(ruleResultList);
return ruleResultList;
}
private async ValueTask ExecuteActionAsync(IEnumerable<RuleResultTree> 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<ActionRuleResult> ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters)
{
var compiledRule = CompileRule(workflowName, ruleName, ruleParameters);
@ -125,11 +131,11 @@ namespace RulesEngine
private async ValueTask<ActionRuleResult> 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);

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>3.2.0</Version>
<Version>3.3.0</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/microsoft/RulesEngine</PackageProjectUrl>
@ -27,12 +27,12 @@
<ItemGroup>
<PackageReference Include="FastExpressionCompiler" Version="3.2.0" />
<PackageReference Include="FluentValidation" Version="10.2.3" />
<PackageReference Include="FluentValidation" Version="10.3.0" />
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.10" />
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.11" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />

View File

@ -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<string, System.Func<Actions.ActionBase>> {
{ "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<WorkflowRules[]>(workflowStr,serializationOptions);
var re = new RulesEngine(workflows, null, reSettings: new ReSettings {
CustomActions = new Dictionary<string, System.Func<Actions.ActionBase>> {
{ "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<string, object> {
{"stringContext", "hello"},
{"intContext",1 },
{"objectContext", new { a = "hello", b = 123 } }
}
}
}
},
}
}
};
}
}
}

View File

@ -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<object> Run(ActionContext context, RuleParameter[] ruleParameters)
{
var stringContext = context.GetContext<string>("stringContext");
var intContext = context.GetContext<int>("intContext");
var objectContext = context.GetContext<object>("objectContext");
return new ValueTask<object>(new {
stringContext,
intContext,
objectContext
});
}
}
}

View File

@ -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, ActionInfo>{
{ ActionTriggerType.onSuccess, new ActionInfo{
Actions = new RuleActions{
OnSuccess = new ActionInfo{
Name = "OutputExpression",
Context = new Dictionary<string, object>{
{"expression", "2*2"}
}
}}
}
}
},
new Rule{
RuleName = "EvaluateRuleTest",
RuleExpressionType = RuleExpressionType.LambdaExpression,
Expression = "1 == 1",
Actions = new Dictionary<ActionTriggerType, ActionInfo>{
{ ActionTriggerType.onSuccess, new ActionInfo{
Actions = new RuleActions{
OnSuccess = new ActionInfo{
Name = "EvaluateRule",
Context = new Dictionary<string, object>{
{"workflowName", "ActionWorkflow"},
{"ruleName","ExpressionOutputRuleTest"}
}
}}
}
}
}

View File

@ -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();

View File

@ -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<RuleResultTree> 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<WorkflowRules[]>(workflowStr, serializationOptions);
var reSettings = new ReSettings { };
var rulesEngine = new RulesEngine(workflowsViaTextJson, reSettings: reSettings);
dynamic input1 = new ExpandoObject();
input1.trueValue = true;
List<RuleResultTree> 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<string, object> {
{ "Expression", "input1.TrueValue" }
}
}
}
},
new Rule {
RuleName = "falseRule1",
Expression = "input1.TrueValue == false",
Actions = new RuleActions {
OnFailure = new ActionInfo{
Name = "OutputExpression",
Context = new Dictionary<string, object> {
{ "Expression", "input1.TrueValue" }
}
}
}
}
},
Actions = new RuleActions {
OnFailure = new ActionInfo{
Name = "OutputExpression",
Context = new Dictionary<string, object> {
{ "Expression", "input1.TrueValue" }
}
}
}
}
}
}
};
}
}

View File

@ -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

View File

@ -6,12 +6,13 @@
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<PackageReference Include="coverlet.collector" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>