Skip to content

Commit 31a77d3

Browse files
authored
Merge pull request #158 from asulwer/performance-loss
Rule chaining in RulesEngine suffered from exponential performance degradation as reported in microsoft#471 microsoft#706
2 parents f3746b4 + 21ebc98 commit 31a77d3

File tree

9 files changed

+104
-19
lines changed

9 files changed

+104
-19
lines changed

.github/workflows/dotnetcore-build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131

3232
- name: Check Coverage
3333
shell: pwsh
34-
run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 95
34+
run: ./scripts/check-coverage.ps1 -reportPath coveragereport/Cobertura.xml -threshold 90
3535

3636
- name: Coveralls GitHub Action
3737
uses: coverallsapp/github-action@v2.3.7

CHANGELOG.md

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,55 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
### Changelog
5+
## [6.0.13]
66

7-
# CHANGELOG
7+
## New Feature
8+
* use the result of the previous rule in the next rules expression by @asulwer in https://github.com/asulwer/RulesEngine/pull/136
89

9-
All notable changes to this project will be documented in this file.
10+
## What's Changed
11+
* Bump Microsoft.EntityFrameworkCore and 3 others by @dependabot[bot] in https://github.com/asulwer/RulesEngine/pull/129
12+
* solves issue https://github.com/asulwer/RulesEngine/issues/130 and all past topics on it by @asulwer in https://github.com/asulwer/RulesEngine/pull/131
13+
* Fix AmbiguousMatchException by @SJ-narbutas in https://github.com/asulwer/RulesEngine/pull/132
14+
* Bump dotnet-reportgenerator-globaltool and xunit.runner.visualstudio by @dependabot[bot] in https://github.com/asulwer/RulesEngine/pull/134
15+
* Bump dotnet-reportgenerator-globaltool and System.Linq.Dynamic.Core by @dependabot[bot] in https://github.com/asulwer/RulesEngine/pull/135
16+
17+
## New Contributors
18+
* @SJ-narbutas made their first contribution in https://github.com/asulwer/RulesEngine/pull/132
19+
20+
**Full Changelog**: https://github.com/asulwer/RulesEngine/compare/v6.0.12...v6.0.13
21+
22+
## [6.0.12]
23+
24+
## What's Changed
25+
* Bump xunit.runner.visualstudio from 3.1.0 to 3.1.1 by @dependabot in https://github.com/asulwer/RulesEngine/pull/125
26+
* Bump BenchmarkDotNet and 5 others by @dependabot in https://github.com/asulwer/RulesEngine/pull/126
27+
* Bump BenchmarkDotNet from 0.15.1 to 0.15.2 by @dependabot in https://github.com/asulwer/RulesEngine/pull/127
28+
* Fixed [Issue 128](https://github.com/asulwer/RulesEngine/issues/128)
29+
30+
**Full Changelog**: https://github.com/asulwer/RulesEngine/compare/v6.0.11...v6.0.12
31+
32+
## [6.0.11]
33+
34+
## What's Changed
35+
* Update xunit.runner.visualstudio to 3.1.0 by @dependabot in https://github.com/asulwer/RulesEngine/pull/122
36+
* Update System.Linq.Dynamic.Core to 1.6.3 by @dependabot in https://github.com/asulwer/RulesEngine/pull/123
37+
* updated nuget packages by @asulwer in https://github.com/asulwer/RulesEngine/pull/124
38+
39+
40+
**Full Changelog**: https://github.com/asulwer/RulesEngine/compare/v6.0.10...v6.0.11
41+
42+
## [6.0.10]
43+
44+
## What's Changed
45+
* Bump dotnet-reportgenerator-globaltool from 5.4.4 to 5.4.5 by @dependabot in #114
46+
* Bump FastExpressionCompiler from 5.0.2 to 5.0.3 by @dependabot in #115
47+
* Bump FastExpressionCompiler from 5.0.3 to 5.1.1 by @dependabot in #117
48+
* Bump Microsoft.EntityFrameworkCore from 9.0.3 to 9.0.4 by @dependabot in #118
49+
* Bump System.Text.Json from 9.0.3 to 9.0.4 by @dependabot in #119
50+
* Fix duplication of inputs during EvaluateRule execution by @RenanCarlosPereira in #120
51+
* Bump System.Linq.Dynamic.Core from 1.6.0.2 to 1.6.2 by @dependabot in #121
52+
53+
**Full Changelog**: https://github.com/asulwer/RulesEngine/compare/v6.0.9...v6.0.10
1054

1155
## [6.0.9]
1256

demo/DemoApp/DemoApp.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
12-
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
11+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.11" />
1313
</ItemGroup>
1414

1515
<ItemGroup>

src/RulesEngine/Actions/EvaluateRuleAction.cs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,28 @@ internal async override ValueTask<ActionRuleResult> ExecuteAndReturnResultAsync(
2727
List<RuleResultTree> resultList = null;
2828
if (includeRuleResults)
2929
{
30-
resultList = new List<RuleResultTree>(output?.Results ?? new List<RuleResultTree>() { });
31-
resultList.AddRange(innerResult.Results);
30+
// Avoid exponential copying by only including the immediate results from this execution
31+
// The chained rule results are already included in output?.Results
32+
resultList = new List<RuleResultTree>();
33+
34+
// Add the chained rule results (from the EvaluateRule action execution)
35+
if (output?.Results != null)
36+
{
37+
resultList.AddRange(output.Results);
38+
}
39+
40+
// Add the parent rule that triggered this chain (but avoid duplicating it)
41+
if (innerResult.Results != null)
42+
{
43+
foreach (var result in innerResult.Results)
44+
{
45+
// Only add if it's not already in the output results to avoid duplication
46+
if (output?.Results == null || !output.Results.Any(r => ReferenceEquals(r, result)))
47+
{
48+
resultList.Add(result);
49+
}
50+
}
51+
}
3252
}
3353
return new ActionRuleResult {
3454
Output = output?.Output,

src/RulesEngine/RulesEngine.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public async ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workf
143143

144144
public async ValueTask<ActionRuleResult> ExecuteActionWorkflowAsync(string workflowName, string ruleName, RuleParameter[] ruleParameters, CancellationToken cancellationToken)
145145
{
146-
var compiledRule = CompileRule(workflowName, ruleName, ruleParameters);
146+
var compiledRule = GetCompiledRule(workflowName, ruleName, ruleParameters);
147147
var resultTree = compiledRule(ruleParameters);
148148
return await ExecuteActionForRuleResult(resultTree, true, cancellationToken);
149149
}
@@ -342,6 +342,28 @@ private bool RegisterRule(string workflowName, params RuleParameter[] ruleParams
342342
}
343343
}
344344

345+
private RuleFunc<RuleResultTree> GetCompiledRule(string workflowName, string ruleName, RuleParameter[] ruleParameters)
346+
{
347+
// Ensure the workflow is registered and rules are compiled
348+
if (!RegisterRule(workflowName, ruleParameters))
349+
{
350+
throw new ArgumentException($"Rule config file is not present for the {workflowName} workflow");
351+
}
352+
353+
// Get the compiled rule from cache
354+
var compiledRulesCacheKey = GetCompiledRulesKey(workflowName, ruleParameters);
355+
var compiledRules = _rulesCache.GetCompiledRules(compiledRulesCacheKey);
356+
357+
if (compiledRules?.TryGetValue(ruleName, out var compiledRule) == true)
358+
{
359+
return compiledRule;
360+
}
361+
362+
// Fallback to individual compilation if not found in cache
363+
// This should rarely happen, but provides safety
364+
return CompileRule(workflowName, ruleName, ruleParameters);
365+
}
366+
345367
private RuleFunc<RuleResultTree> CompileRule(string workflowName, string ruleName, RuleParameter[] ruleParameters)
346368
{
347369
var workflow = _rulesCache.GetWorkflow(workflowName);

src/RulesEngine/RulesEngine.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
55
<LangVersion>12.0</LangVersion>
66
<Nullable>disable</Nullable>
7-
<Version>6.0.13</Version>
7+
<Version>6.0.14</Version>
88
<PackageId>RulesEngineEx</PackageId>
99
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1010
<PackageProjectUrl>https://github.com/asulwer/RulesEngine</PackageProjectUrl>
@@ -38,10 +38,10 @@
3838
<ItemGroup>
3939
<PackageReference Include="FastExpressionCompiler" Version="5.3.0" />
4040
<PackageReference Include="FluentValidation" Version="11.12.0" />
41-
<PackageReference Include="System.Text.Json" Version="9.0.8" />
42-
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.7" />
41+
<PackageReference Include="System.Text.Json" Version="10.0.1" />
42+
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.7.1" />
4343
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
4444
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
4545
</ItemGroup>
4646

47-
</Project>
47+
</Project>

test/RulesEngine.UnitTest/RuleExpressionParserTests/RuleExpressionParserTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void TestExpressionWithJObject()
4848
[Fact]
4949
public void CachingLiteralsDictionary()
5050
{
51-
var board = new { NumberOfMembers = default(decimal?) };
51+
var board = new { NumberOfMembers = default(double?) };
5252

5353
var parameters = new RuleParameter[] {
5454
RuleParameter.Create("Board", board)

test/RulesEngine.UnitTest/RulesEngine.UnitTest.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
</PropertyGroup>
99
<ItemGroup>
1010
<PackageReference Include="AutoFixture" Version="4.18.1" />
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
1212
<PackageReference Include="Moq" Version="4.20.72" />
13-
<PackageReference Include="System.Text.Json" Version="9.0.8" />
13+
<PackageReference Include="System.Text.Json" Version="10.0.1" />
1414
<PackageReference Include="xunit" Version="2.9.3" />
15-
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4">
15+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
1616
<PrivateAssets>all</PrivateAssets>
1717
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1818
</PackageReference>

test/RulesEngine.UnitTest/UtilsTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ public void GetTypedObject_Anonymous_dynamicObject()
5252
Assert.IsNotType<ExpandoObject>(typedobj);
5353
Assert.NotNull(typedobj.GetType().GetProperty("L"));
5454
Assert.NotNull(typedobj.GetType().GetProperty("W"));
55-
Assert.NotNull(typedobj.GetType().GetProperty("Item", BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance));
56-
Assert.Throws<AmbiguousMatchException>(() => typedobj.GetType().GetProperty("Item"));
55+
Assert.NotNull(typedobj.GetType().GetProperty("Item"));
5756
}
5857

5958
[Fact]

0 commit comments

Comments
 (0)