Skip to content

Fix #624, #704, #711, #714, #717: action exception propagation, list schema merging, OutputExpression error hint, global-param dedup, object-return diagnostics#728

Merged
YogeshPraj merged 2 commits into
microsoft:mainfrom
YogeshPraj:fix/issues-batch-1
May 27, 2026

Conversation

@YogeshPraj
Copy link
Copy Markdown
Contributor

Summary

Bundles five independent bug fixes that came out of the issue triage following #727. Each fix has a dedicated regression test file; details below.

Fixes #624EnableExceptionAsErrorMessage = false silently suppressed action exceptions

ActionBase.ExecuteAndReturnResultAsync catches every exception thrown from Run() and stuffs it into ActionRuleResult.Exception, which the engine then quietly stores on the result tree. With EnableExceptionAsErrorMessage = false, the docs say the engine should throw — but action-level exceptions never propagated.

Added ThrowIfActionExceptionShouldPropagate in RulesEngine.cs. After the action result is captured, if EnableExceptionAsErrorMessage == false && IgnoreException == false and the action produced an exception, we re-throw. Both ExecuteAllRulesAsync and ExecuteActionWorkflowAsync paths now honor the contract.

Fixes #711 — Cryptic error for C#-style anonymous objects in OutputExpression

Users frequently write "new { State = input.state, ... } as Result" (C# syntax), and Dynamic.Core responds with "Expression is missing an 'as' clause". The correct Dynamic.Core form is "new (input.state as State, ...)".

OutputExpressionAction.Run now catches ParseException when the expression matches \bnew\s*\{ and wraps it with a clear hint about the required syntax.

Fixes #714 — Global params evaluated once per rule instead of once per workflow execution

RuleCompiler.GetWrappedRuleFunc wrapped each rule's compiled delegate with a per-invocation re-evaluation of all global+local scoped params. A Utils.FromDb(...) call in GlobalParams therefore ran once for every rule in the workflow.

Refactor:

  1. RuleCompiler.CompileRule no longer wraps with globals — it returns the inner delegate that expects globals as part of RuleParameter[].
  2. RulesEngine.RegisterRule eagerly evaluates global expressions, compiles a single Func<object[], Dictionary<string, object>>, and stores it in a new per-workflow slot on RulesCache.
  3. ExecuteAllRuleByWorkflow invokes that delegate once with the user inputs, appends the result as extra RuleParameters, and passes the extended array to each compiled rule.
  4. Compile-time and runtime failures in globals are caught and surfaced as per-rule error messages ("Error while compiling rule …" / "Error while executing scoped params for rule …"), preserving the contract that existing tests rely on.
  5. ExecuteActionWorkflowAsync (which bypasses RulesCache) evaluates globals ad-hoc.

Fixes #704Utils.CreateAbstractClassType lost properties that only appeared in later list elements

CreateAbstractClassType and CreateAbstractClassTypeFromDictionary both inferred the CLR element type for an IList from list[0] only, so any property that appeared first in a later element was silently dropped during CreateObject/CreateObjectFromDictionary.

Added a BuildListType helper that walks every element and unions their schemas via MergeListElementSchemas + MergeTwoDictionaries. Nested dictionaries are recursively merged. Non-schema-like elements (primitives, strings) keep the previous first-element behavior.

Fixes #717 — No diagnostic when a custom method returns object

Dynamic.Core fails with "No property or field 'X' exists in type 'Object'" or "is not defined for the types 'System.Object' and ..." when a custom/static method's declared return type is object, because the parser only has the static signature. We can't unbox at parse time, but we can tell the user what's wrong.

LambdaExpressionBuilder.BuildDelegateForRule detects these patterns in ex.Message and appends a hint telling the user to change the method's return type to the concrete class.

Test plan

  • 5 new test files (Issue624Test, Issue711Test, Issue714Test, Issue704Test, Issue717Test) — 13 new tests total, all covering both the bug repro and the regression guard.
  • All 144 unit tests pass on net6.0 / net8.0 / net9.0 / net10.0.
  • No changes to public API surface; behavior changes only on the documented-but-broken paths.

Note on what's deliberately out of scope

Yogesh Prajapati added 2 commits May 27, 2026 16:17
…soft#624: bug bash batch

microsoft#624: ActionBase.ExecuteAndReturnResultAsync silently captured every
exception into result.Exception regardless of EnableExceptionAsErrorMessage,
so an action throwing inside Run() never propagated when the setting was
false. Add ThrowIfActionExceptionShouldPropagate in RulesEngine to honor
the contract documented on ReSettings.

microsoft#711: OutputExpressionAction emitted a cryptic
"Expression is missing an 'as' clause" when users wrote C#-style anonymous
objects (`new { X = ... } as Result`). Detect the pattern and wrap the
parse exception with a clear hint pointing at the Dynamic.Core syntax
(`new (value as Name, ...)`).

microsoft#714: Global params were re-evaluated for every rule in the workflow,
so `Utils.FromDb(myInput)` ran N times per ExecuteAllRulesAsync call.
Move the global-params compilation+evaluation to workflow scope: compile
a single delegate at RegisterRule time, store it in RulesCache, evaluate
once in ExecuteAllRuleByWorkflow and append the result as RuleParameters
to each compiled rule. Preserve the existing per-rule error messages when
global compilation or evaluation fails. ExecuteActionWorkflowAsync
(which bypasses RulesCache) evaluates globals ad-hoc.

microsoft#704: Utils.CreateAbstractClassType used only list[0] when generating the
CLR type for a heterogeneous IList of ExpandoObject/Dictionary, so any
property appearing only in later elements was dropped. Walk every element
and union the schema; recursively merge nested dictionaries.

microsoft#717: Methods declared to return `object` cannot have their result
members accessed in Dynamic.Core expressions — the parser errors with
"exists in type 'Object'" or "is not defined for the types 'System.Object'".
We can't unbox at parse time, but the error message gave no clue what was
wrong. Detect those patterns in LambdaExpressionBuilder and append a
hint to change the method's return type to the concrete class.

All 144 tests pass on net6.0 / net8.0 / net9.0 / net10.0.
- RulesEngine.cs: ApplyGlobalParams and EvaluateGlobalsAdHoc both built
  RuleParameters from a globals delegate and concatenated them with the
  user's inputs. Extracted AppendGlobals (delegate-invoke + concat tail)
  and CompileGlobalParamsDelegate (GetRuleExpressionParameters + CompileScopedParams).

- Utils.cs: MergeListElementSchemas and MergeTwoDictionaries shared the
  same pair-merge logic. Replaced both with MergeDictionaries (n-ary fold)
  + MergeValues (handles dict/dict, list/list, and first-non-null fallback).
  The latter also makes nested list-concatenation consistent across all
  call sites.

No behavior change. All 144 tests still pass on net6/8/9/10.
@YogeshPraj YogeshPraj merged commit 29913a6 into microsoft:main May 27, 2026
3 checks passed
@YogeshPraj YogeshPraj mentioned this pull request May 27, 2026
2 tasks
YogeshPraj added a commit that referenced this pull request May 27, 2026
Bump <Version> from 6.0.0 to 6.0.1-preview.1 and add a CHANGELOG entry
covering the three PRs landed since 6.0.0: #727 (perf cache restore),
#728 (action-exception propagation, list schema union, OutputExpression
hint, global-param dedup, object-return diagnostics), and #729
(ExecuteActionWorkflowAsync FormatErrorMessages, ActionContext null guard,
deep ErrorMessage interpolation, plus regression guards for #581, #590,
#606, #608).

Co-authored-by: Yogesh Prajapati <yogeshcprajapati@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment