diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53a104b3..beaf7f00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,13 @@
All notable changes to this project will be documented in this file.
+## [Unreleased]
+
+### Features
+- `IRulesEngine.ExecuteAllRulesAsync` gains an overload accepting a `CancellationToken`, observed cooperatively between rules and before each action. The existing `params object[]` and `params RuleParameter[]` overloads are unchanged; call-site overload resolution continues to pick them when no token is supplied (#609).
+- New `ReSettings.AutoExecuteActions` (default `true`). Set to `false` to evaluate rules without automatically running their OnSuccess/OnFailure actions, so callers can run actions selectively via `ExecuteActionWorkflowAsync` (#596).
+- Documented and tested passing computed `additionalInputs` into the `EvaluateRule` action — the additionalInput `Name` is referenced directly in the target rule's expression (#573).
+
## [6.0.1-preview.1]
### Performance
diff --git a/src/RulesEngine/Interfaces/IRulesEngine.cs b/src/RulesEngine/Interfaces/IRulesEngine.cs
index 000bdfc3..aac2cd4b 100644
--- a/src/RulesEngine/Interfaces/IRulesEngine.cs
+++ b/src/RulesEngine/Interfaces/IRulesEngine.cs
@@ -3,6 +3,7 @@
using RulesEngine.Models;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
namespace RulesEngine.Interfaces
@@ -24,6 +25,16 @@ public interface IRulesEngine
/// A variable number of rule parameters
/// List of rule results
ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams);
+
+ ///
+ /// This will execute all the rules of the specified workflow with cooperative cancellation.
+ ///
+ /// The name of the workflow with rules to execute against the inputs
+ /// The rule parameters
+ /// Token observed between rules and before each action
+ /// List of rule results
+ ValueTask> ExecuteAllRulesAsync(string workflowName, RuleParameter[] ruleParams, CancellationToken cancellationToken);
+
ValueTask ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters);
///
@@ -41,8 +52,8 @@ public interface IRulesEngine
/// Removes the workflow from RulesEngine
///
///
- void RemoveWorkflow(params string[] workflowNames);
-
+ void RemoveWorkflow(params string[] workflowNames);
+
///
/// Checks is workflow exist.
///
diff --git a/src/RulesEngine/Models/ReSettings.cs b/src/RulesEngine/Models/ReSettings.cs
index 247d637a..a4a4262c 100644
--- a/src/RulesEngine/Models/ReSettings.cs
+++ b/src/RulesEngine/Models/ReSettings.cs
@@ -29,6 +29,7 @@ internal ReSettings(ReSettings reSettings)
AutoRegisterInputType = reSettings.AutoRegisterInputType;
UseFastExpressionCompiler = reSettings.UseFastExpressionCompiler;
EnableExceptionAsErrorMessageForRuleExpressionParsing = reSettings.EnableExceptionAsErrorMessageForRuleExpressionParsing;
+ AutoExecuteActions = reSettings.AutoExecuteActions;
}
@@ -90,6 +91,13 @@ internal ReSettings(ReSettings reSettings)
/// Default: true
///
public bool EnableExceptionAsErrorMessageForRuleExpressionParsing { get; set; } = true;
+
+ ///
+ /// When true (default), ExecuteAllRulesAsync automatically runs each matched rule's
+ /// OnSuccess/OnFailure action after evaluation. Set to false to evaluate rules only and
+ /// run actions yourself (e.g. via ExecuteActionWorkflowAsync) for selective control. See #596.
+ ///
+ public bool AutoExecuteActions { get; set; } = true;
}
public enum NestedRuleExecutionMode
diff --git a/src/RulesEngine/RulesEngine.cs b/src/RulesEngine/RulesEngine.cs
index 74d6a307..a7d715ff 100644
--- a/src/RulesEngine/RulesEngine.cs
+++ b/src/RulesEngine/RulesEngine.cs
@@ -14,6 +14,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
+using System.Threading;
using System.Threading.Tasks;
namespace RulesEngine
@@ -102,21 +103,39 @@ public async ValueTask> ExecuteAllRulesAsync(string workflo
/// A variable number of rule parameters
/// List of rule results
public async ValueTask> ExecuteAllRulesAsync(string workflowName, params RuleParameter[] ruleParams)
+ {
+ return await ExecuteAllRulesAsync(workflowName, ruleParams, default);
+ }
+
+ ///
+ /// This will execute all the rules of the specified workflow with cooperative cancellation.
+ ///
+ /// The name of the workflow with rules to execute against the inputs
+ /// The rule parameters
+ /// Token observed between rules and before each action. A single
+ /// rule's compiled expression is not interrupted mid-evaluation; cancellation is cooperative at
+ /// rule and action boundaries.
+ /// List of rule results
+ public async ValueTask> ExecuteAllRulesAsync(string workflowName, RuleParameter[] ruleParams, CancellationToken cancellationToken)
{
var sortedRuleParams = ruleParams.ToList();
sortedRuleParams.Sort((RuleParameter a, RuleParameter b) => string.Compare(a.Name, b.Name));
- var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, sortedRuleParams.ToArray());
- await ExecuteActionAsync(ruleResultList);
+ var ruleResultList = ValidateWorkflowAndExecuteRule(workflowName, sortedRuleParams.ToArray(), cancellationToken);
+ if (_reSettings.AutoExecuteActions)
+ {
+ await ExecuteActionAsync(ruleResultList, cancellationToken);
+ }
return ruleResultList;
}
- private async ValueTask ExecuteActionAsync(IEnumerable ruleResultList)
+ private async ValueTask ExecuteActionAsync(IEnumerable ruleResultList, CancellationToken cancellationToken = default)
{
foreach (var ruleResult in ruleResultList)
{
+ cancellationToken.ThrowIfCancellationRequested();
if(ruleResult.ChildResults != null)
{
- await ExecuteActionAsync(ruleResult.ChildResults);
+ await ExecuteActionAsync(ruleResult.ChildResults, cancellationToken);
}
var actionResult = await ExecuteActionForRuleResult(ruleResult, false);
ruleResult.ActionResult = new ActionResult {
@@ -304,13 +323,13 @@ public void RemoveWorkflow(params string[] workflowNames)
/// input
/// workflow name
/// list of rule result set
- private List ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams)
+ private List ValidateWorkflowAndExecuteRule(string workflowName, RuleParameter[] ruleParams, CancellationToken cancellationToken = default)
{
List result;
if (RegisterRule(workflowName, ruleParams))
{
- result = ExecuteAllRuleByWorkflow(workflowName, ruleParams);
+ result = ExecuteAllRuleByWorkflow(workflowName, ruleParams, cancellationToken);
}
else
{
@@ -430,7 +449,7 @@ private static void CollectAllElementTypes(Type t, ISet collector)
///
///
/// list of rule result set
- private List ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters)
+ private List ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters, CancellationToken cancellationToken = default)
{
var result = new List();
var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters);
@@ -456,6 +475,7 @@ private List ExecuteAllRuleByWorkflow(string workflowName, RuleP
foreach (var compiledRule in compiledRules)
{
+ cancellationToken.ThrowIfCancellationRequested();
RuleResultTree resultTree;
if (globalEvaluationException != null && ruleByName != null && ruleByName.TryGetValue(compiledRule.Key, out var rule))
{
diff --git a/test/RulesEngine.UnitTest/Issue573Test.cs b/test/RulesEngine.UnitTest/Issue573Test.cs
new file mode 100644
index 00000000..094c0609
--- /dev/null
+++ b/test/RulesEngine.UnitTest/Issue573Test.cs
@@ -0,0 +1,68 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using RulesEngine.Models;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace RulesEngine.UnitTest
+{
+ [ExcludeFromCodeCoverage]
+ public class Issue573Test
+ {
+ // Demonstrates the correct way to pass computed additionalInputs into an EvaluateRule
+ // action. The target rule can reference the additionalInput by its Name. The key detail
+ // (the source of the "Unknown identifier" confusion in #573) is that the additionalInput
+ // Name must match the identifier used in the target rule's expression.
+ [Fact]
+ public async Task EvaluateRuleAction_AdditionalInputs_AreAvailableToTargetRule()
+ {
+ var workflow = new Workflow
+ {
+ WorkflowName = "wf",
+ Rules = new[]
+ {
+ new Rule
+ {
+ RuleName = "Parent",
+ Expression = "input1.Value > 0",
+ Actions = new RuleActions
+ {
+ OnSuccess = new ActionInfo
+ {
+ Name = "EvaluateRule",
+ Context = new Dictionary
+ {
+ { "workflowName", "wf" },
+ { "ruleName", "Child" },
+ // Compute a new input named "doubled" from input1 and pass it on.
+ { "additionalInputs", new List
+ {
+ new ScopedParam { Name = "doubled", Expression = "input1.Value * 2" }
+ }
+ }
+ }
+ }
+ }
+ },
+ new Rule
+ {
+ RuleName = "Child",
+ // References the additionalInput by name.
+ Expression = "doubled == 20"
+ }
+ }
+ };
+
+ var engine = new RulesEngine(new[] { workflow });
+ var result = await engine.ExecuteActionWorkflowAsync("wf", "Parent",
+ new[] { RuleParameter.Create("input1", new { Value = 10 }) });
+
+ // Child rule succeeded because "doubled" (10*2=20) was available to it.
+ Assert.NotNull(result.Results);
+ Assert.Contains(result.Results, r => r.Rule.RuleName == "Child" && r.IsSuccess);
+ }
+ }
+}
diff --git a/test/RulesEngine.UnitTest/Issue596Test.cs b/test/RulesEngine.UnitTest/Issue596Test.cs
new file mode 100644
index 00000000..9d341c87
--- /dev/null
+++ b/test/RulesEngine.UnitTest/Issue596Test.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using RulesEngine.Actions;
+using RulesEngine.Models;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace RulesEngine.UnitTest
+{
+ [ExcludeFromCodeCoverage]
+ public class Issue596Test
+ {
+ private class CountingAction : ActionBase
+ {
+ public static int RunCount;
+ public override ValueTask