Route ExecuteActionWorkflowAsync through RulesCache (fixes #471)#734
Merged
Conversation
) ExecuteActionWorkflowAsync previously called a private CompileRule(workflowName, ruleName, ...) overload that always rebuilt the per-rule wrapper delegates from scratch — no RulesCache lookup. With EvaluateRuleAction this turned every link in a rule chain into a fresh compilation pass even when the workflow was already registered. This change routes the method through the same RegisterRule + GetCompiledRules + ApplyGlobalParams path that ExecuteAllRulesAsync uses. First call still compiles; subsequent calls with the same workflow + param shape are cache hits. Globals are evaluated once via the workflow-level cached delegate, matching the existing microsoft#714 behavior. Side cleanups: - The now-unused private CompileRule(workflowName, ruleName, ruleParameters) overload is removed. - EvaluateGlobalsAdHoc and CompileGlobalParamsDelegate are gone — the cache path supersedes them. Benchmark (median of 5 runs, net8.0, 10-deep rule chain via EvaluateRuleAction): Chain x 100 invocations: 7 ms -> 3 ms (~2.3x) Chain x 1,000 invocations: 65 ms -> 42 ms (~1.5x) The remaining time is delegate invocation + ActionContext setup; further wins would need ActionContext JSON-serialization to be reworked, which is out of scope here. Also adds regression tests for both microsoft#471 and microsoft#513: - Issue471Test verifies the cached path, chain correctness, and per-workflow cache-key isolation. - Issue513Test documents the recommended OR-short-circuit pattern (NestedRuleExecutionMode.Performance under a single OrElse parent rule) for workflows that want "include if any rule matches" semantics. All 160 unit tests pass on net6 / net8 / net9 / net10.
sagarambilpure
approved these changes
May 29, 2026
This was referenced May 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Tier-3 perf follow-up. Two real outcomes plus a write-up for the third.
Fixes #471 — Rule-chaining (
EvaluateRuleAction) recompiled the target rule on every linkExecuteActionWorkflowAsynccalled a privateCompileRule(workflowName, ruleName, ruleParameters)overload that always rebuilt the per-rule wrapper delegates from scratch — noRulesCachelookup. WithEvaluateRuleActionthis turned every link in a chain into a fresh compilation pass even when the workflow was already registered.Routed
ExecuteActionWorkflowAsyncthrough the sameRegisterRule+GetCompiledRules+ApplyGlobalParamspath thatExecuteAllRulesAsyncuses. First call still compiles; subsequent calls with the same workflow + param shape are cache hits. Globals are evaluated once via the workflow-level cached delegate (consistent with the #714 model).Side cleanups:
CompileRule(workflowName, ruleName, …)overload is removed.EvaluateGlobalsAdHocandCompileGlobalParamsDelegate(introduced in Fix #624, #704, #711, #714, #717: action exception propagation, list schema merging, OutputExpression error hint, global-param dedup, object-return diagnostics #728 as a fallback for the un-cached path) are gone — the cache path supersedes them.Benchmark (median of 5 runs, .NET 8, 10-deep rule chain via
EvaluateRuleAction)The remaining time is delegate invocation +
ActionContextJSON-serialization. Reworking the latter would be the next obvious win but is out of scope here.Addresses #513 — "13 rules × 600 records = 13 seconds" with a documented pattern
The original 13-second number was almost certainly the v5.0.1 cache regression (fixed in #727). On
6.0.1-preview.1the same scenario completes in ~5 ms for the no-short-circuit case and ~2 ms with the recommended pattern. No code change needed.Added
Issue513Testthat documents the recommended OR-short-circuit pattern:Operator = "OrElse".ReSettings.NestedRuleExecutionMode = NestedRuleExecutionMode.Performance.The test asserts the short-circuit fires (
ChildResults.Count == 1when the first child matches) and the no-short-circuit baseline still runs all 13.Defers #707 — vague repro, no fix in this PR
The reporter said "after upgrading to 6.02 … much slower performance … a lot of first chance exceptions thrown, dynamic assembly types I think." No code, no benchmark numbers, no version string we can pin down (6.02 is ambiguous).
I'd recommend closing this in favor of asking the reporter for:
6.0.1-preview.1package (which contains the Restore compiled-expression cache in RuleExpressionParser (fixes #673) #727 cache restoration that fixed the major 5.0.1 regression)Happy to do that as a comment on the issue rather than in this PR.
Test plan
Issue471Test.cs,Issue513Test.cs) — 5 new tests.RulesEngineWithActionsTests(which exercisesExecuteActionWorkflowAsyncandEvaluateRuleAction) still passes — confirms the cache-routing refactor preserves behavior.