Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "9.0.0",
"version": "8.0.117",
"rollForward": "latestFeature",
"allowPrerelease": false
}
Expand Down
180 changes: 157 additions & 23 deletions src/RulesEngine/RulesEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,31 +368,165 @@ private static void CollectAllElementTypes(Type t, ISet<Type> collector)
}
}

/// <summary>
/// This will execute the compiled rules
/// </summary>
/// <param name="workflowName"></param>
/// <param name="ruleParams"></param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters)
{
var result = new List<RuleResultTree>();
var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters);
foreach (var compiledRule in _rulesCache.GetCompiledRules(compiledRulesCacheKey)?.Values)
{
var resultTree = compiledRule(ruleParameters);
result.Add(resultTree);
}

FormatErrorMessages(result);
return result;
/// <summary>
/// This will execute the compiled rules
/// </summary>
/// <param name="workflowName"></param>
/// <param name="ruleParams"></param>
/// <returns>list of rule result set</returns>
private List<RuleResultTree> ExecuteAllRuleByWorkflow(string workflowName, RuleParameter[] ruleParameters)
{
var result = new List<RuleResultTree>();
var workflow = _rulesCache.GetWorkflow(workflowName);
if (workflow == null)
{
return result;
}

var extendedRuleParameters = new List<RuleParameter>(ruleParameters);
var ruleResults = new Dictionary<string, bool>();
var successEvents = new HashSet<string>();

foreach (var rule in workflow.Rules.Where(c => c.Enabled))
{
// Check if the rule expression contains rule references
var hasRuleReferences = ContainsRuleReferences(rule.Expression, ruleResults.Keys) || ContainsSuccessEventReferences(rule.Expression, successEvents);

RuleFunc<RuleResultTree> compiledRule;

if (hasRuleReferences && _reSettings.EnableScopedParams)
{
// Compile rule with additional scoped parameters for rule results
compiledRule = CompileRuleWithRuleResults(rule, workflow.RuleExpressionType, extendedRuleParameters.ToArray(), ruleResults, successEvents, workflow);
}
else
{
// Use standard compilation
var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters);
var cachedRules = _rulesCache.GetCompiledRules(compiledRulesCacheKey);
compiledRule = cachedRules?.ContainsKey(rule.RuleName) == true ? cachedRules[rule.RuleName] : null;

if (compiledRule == null)
{
// Fallback compilation if not in cache
var globalParamExp = new Lazy<RuleExpressionParameter[]>(
() => _ruleCompiler.GetRuleExpressionParameters(workflow.RuleExpressionType, workflow.GlobalParams, ruleParameters)
);
compiledRule = CompileRule(rule, workflow.RuleExpressionType, ruleParameters, globalParamExp);
}
}

var resultTree = compiledRule(extendedRuleParameters.ToArray());
result.Add(resultTree);

// Add rule result for future rule references
ruleResults[rule.RuleName] = resultTree.IsSuccess;

// Add success event if rule passed
if (resultTree.IsSuccess && !string.IsNullOrEmpty(rule.SuccessEvent))
{
successEvents.Add(rule.SuccessEvent);
}
}

FormatErrorMessages(result);
return result;
}

private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams)
{
var ruleParamsKey = string.Join("-", ruleParams.Select(c => $"{c.Name}_{c.Type.Name}"));
var key = $"{workflowName}-" + ruleParamsKey;
return key;
private bool ContainsRuleReferences(string expression, IEnumerable<string> availableRuleNames)
{
if (string.IsNullOrEmpty(expression))
return false;

foreach (var ruleName in availableRuleNames)
{
if (expression.Contains($"@{ruleName}"))
return true;
}
return false;
}

private bool ContainsSuccessEventReferences(string expression, IEnumerable<string> availableSuccessEvents)
{
if (string.IsNullOrEmpty(expression))
return false;

foreach (var eventName in availableSuccessEvents)
{
if (expression.Contains(eventName))
return true;
}
return false;
}

private RuleFunc<RuleResultTree> CompileRuleWithRuleResults(Rule rule, RuleExpressionType ruleExpressionType, RuleParameter[] ruleParameters, Dictionary<string, bool> ruleResults, HashSet<string> successEvents, Workflow workflow = null)
{
var globalParamExp = new Lazy<RuleExpressionParameter[]>(
() => _ruleCompiler.GetRuleExpressionParameters(ruleExpressionType, workflow?.GlobalParams, ruleParameters)
);

// Create additional scoped parameters for rule results and success events
var additionalScopedParams = new List<ScopedParam>();

// Preprocess the expression to replace @RuleName with RuleName
var processedExpression = rule.Expression;

// Add rule results as scoped parameters
foreach (var kvp in ruleResults)
{
additionalScopedParams.Add(new ScopedParam
{
Name = kvp.Key,
Expression = kvp.Value.ToString().ToLower()
});

// Replace @RuleName references in the expression
processedExpression = processedExpression.Replace($"@{kvp.Key}", kvp.Key);
}

// Add success events as scoped parameters
foreach (var eventName in successEvents)
{
additionalScopedParams.Add(new ScopedParam
{
Name = eventName,
Expression = "true"
});
}

// Combine with existing local params
var combinedLocalParams = new List<ScopedParam>();
if (rule.LocalParams != null)
{
combinedLocalParams.AddRange(rule.LocalParams);
}
combinedLocalParams.AddRange(additionalScopedParams);

// Create a modified rule with the additional scoped parameters and processed expression
var modifiedRule = new Rule
{
RuleName = rule.RuleName,
Expression = processedExpression,
RuleExpressionType = rule.RuleExpressionType,
LocalParams = combinedLocalParams,
SuccessEvent = rule.SuccessEvent,
ErrorMessage = rule.ErrorMessage,
Enabled = rule.Enabled,
Actions = rule.Actions,
Operator = rule.Operator,
Properties = rule.Properties,
Rules = rule.Rules,
WorkflowsToInject = rule.WorkflowsToInject
};

return CompileRule(modifiedRule, ruleExpressionType, ruleParameters, globalParamExp);
}

private string GetCompiledRulesKey(string workflowName, RuleParameter[] ruleParams)
{
var ruleParamsKey = string.Join("-", ruleParams.Select(c => $"{c.Name}_{c.Type.Name}"));
var key = $"{workflowName}-" + ruleParamsKey;
return key;
}

private IDictionary<string, Func<ActionBase>> GetDefaultActionRegistry()
Expand Down
4 changes: 2 additions & 2 deletions src/RulesEngine/RulesEngine.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;net8.0;net9.0;netstandard2.0</TargetFrameworks>
<LangVersion>13.0</LangVersion>
<TargetFrameworks>net6.0;net8.0;netstandard2.0</TargetFrameworks>
<LangVersion>12.0</LangVersion>
<Version>6.0.0</Version>
<Copyright>Copyright (c) Microsoft Corporation.</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
Expand Down
136 changes: 136 additions & 0 deletions test/RulesEngine.UnitTest/OriginalIssueTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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.Threading.Tasks;
using Xunit;

namespace RulesEngine.UnitTest
{
[ExcludeFromCodeCoverage]
public class OriginalIssueTest
{
[Fact]
public async Task OriginalIssue_RuleChainingWithJsonConfig_ShouldWork()
{
// This test recreates the exact scenario from the GitHub issue
var ruleJson = @"[
{
""WorkflowName"": ""Tuning"",
""Rules"": [
{
""RuleName"": ""EvaluationExpression"",
""SuccessEvent"": ""EvaluationExpressionPassed"",
""ErrorMessage"": ""EvaluationExpression not met."",
""Expression"": ""metrics.current_value > 1000"",
""RuleExpressionType"": ""LambdaExpression""
},
{
""RuleName"": ""VerificationExpression"",
""SuccessEvent"": ""VerificationExpressionPassed"",
""ErrorMessage"": ""VerificationExpression failed."",
""Expression"": ""@EvaluationExpression && metrics.cost_limit >= -1"",
""RuleExpressionType"": ""LambdaExpression""
},
{
""RuleName"": ""ActionExpression"",
""SuccessEvent"": ""ActionExpressionApplied"",
""ErrorMessage"": ""ActionExpression skipped."",
""Expression"": ""VerificationExpressionPassed"",
""RuleExpressionType"": ""LambdaExpression""
}
]
}
]";

var workflows = JsonConvert.DeserializeObject<List<Workflow>>(ruleJson);

// Mock the MetricsProvider.GetSampleMetrics() call
var metrics = new
{
current_value = 2000, // > 1000, so EvaluationExpression should pass
cost_limit = 100 // >= -1, so the cost_limit condition should pass
};

var inputs = new RuleParameter("metrics", metrics);
var engine = new global::RulesEngine.RulesEngine(workflows.ToArray(), new ReSettings { EnableScopedParams = true });
var results = await engine.ExecuteAllRulesAsync("Tuning", inputs);

// Verify all rules pass as expected
Assert.Equal(3, results.Count);

// EvaluationExpression should pass (current_value 2000 > 1000)
Assert.True(results[0].IsSuccess,
$"EvaluationExpression should pass but failed: {results[0].ExceptionMessage}");
Assert.Equal("EvaluationExpression", results[0].Rule.RuleName);

// VerificationExpression should pass (@EvaluationExpression is true AND cost_limit 100 >= -1)
Assert.True(results[1].IsSuccess,
$"VerificationExpression should pass but failed: {results[1].ExceptionMessage}");
Assert.Equal("VerificationExpression", results[1].Rule.RuleName);

// ActionExpression should pass (VerificationExpressionPassed is available and true)
Assert.True(results[2].IsSuccess,
$"ActionExpression should pass but failed: {results[2].ExceptionMessage}");
Assert.Equal("ActionExpression", results[2].Rule.RuleName);

// Verify success events are properly set
Assert.Equal("EvaluationExpressionPassed", results[0].Rule.SuccessEvent);
Assert.Equal("VerificationExpressionPassed", results[1].Rule.SuccessEvent);
Assert.Equal("ActionExpressionApplied", results[2].Rule.SuccessEvent);
}

[Fact]
public async Task OriginalIssue_FailureScenario_ShouldHandleCorrectly()
{
// Test the scenario where the first rule fails
var ruleJson = @"[
{
""WorkflowName"": ""Tuning"",
""Rules"": [
{
""RuleName"": ""EvaluationExpression"",
""SuccessEvent"": ""EvaluationExpressionPassed"",
""ErrorMessage"": ""EvaluationExpression not met."",
""Expression"": ""metrics.current_value > 1000"",
""RuleExpressionType"": ""LambdaExpression""
},
{
""RuleName"": ""VerificationExpression"",
""SuccessEvent"": ""VerificationExpressionPassed"",
""ErrorMessage"": ""VerificationExpression failed."",
""Expression"": ""@EvaluationExpression && metrics.cost_limit >= -1"",
""RuleExpressionType"": ""LambdaExpression""
}
]
}
]";

var workflows = JsonConvert.DeserializeObject<List<Workflow>>(ruleJson);

// Create metrics where EvaluationExpression will fail
var metrics = new
{
current_value = 500, // < 1000, so EvaluationExpression should fail
cost_limit = 100 // >= -1, but this won't matter because @EvaluationExpression is false
};

var inputs = new RuleParameter("metrics", metrics);
var engine = new global::RulesEngine.RulesEngine(workflows.ToArray(), new ReSettings { EnableScopedParams = true });
var results = await engine.ExecuteAllRulesAsync("Tuning", inputs);

Assert.Equal(2, results.Count);

// EvaluationExpression should fail (current_value 500 <= 1000)
Assert.False(results[0].IsSuccess, "EvaluationExpression should fail");
Assert.Equal("EvaluationExpression", results[0].Rule.RuleName);

// VerificationExpression should fail (@EvaluationExpression is false, so entire expression is false)
Assert.False(results[1].IsSuccess, "VerificationExpression should fail when EvaluationExpression fails");
Assert.Equal("VerificationExpression", results[1].Rule.RuleName);
}
}
}
Loading
Loading