Skip to content

Add .NET 10 support: fix InvalidProgramException in expression compilation#1429

Open
danielgerlag wants to merge 4 commits into
masterfrom
fix/net10-expression-compilation
Open

Add .NET 10 support: fix InvalidProgramException in expression compilation#1429
danielgerlag wants to merge 4 commits into
masterfrom
fix/net10-expression-compilation

Conversation

@danielgerlag

@danielgerlag danielgerlag commented May 16, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #1428InvalidProgramException from DefinitionLoader.BuildScalarInputAction when running on .NET 10.

Root Cause

Several methods in DefinitionLoader.cs called LambdaExpression.Compile() inside closures / local functions that captured the expression objects, and recompiled on every invocation. On .NET 10's updated expression-tree compiler this pattern can produce invalid IL at runtime.

Changes

Expression pre-compilation (DefinitionLoader.cs)

  • BuildScalarInputAction: compile the expression before the local function instead of on each invocation.
  • BuildObjectInputAction: pre-scan the JObject template and compile all @-prefixed expressions at definition-load time.
  • AttachDirectlyOutput: pre-compile the source expression before the lambda.
  • AttachNestedOutput: pre-compile both target and source expressions before the lambda.

Expression pre-compilation (MemberMapParameter.cs)

  • Cache the compiled source delegate in the constructor instead of recompiling on every Assign call.

Regression test (issue #1428)

  • Added a targeted test mirroring the reporter's exact scenario: a scalar variable-binding input (data.MessageId) plus a scalar string-literal input ("waits-for-batching"). It loads the definition and assigns the inputs the same way WorkflowExecutor.ExecuteStep does, asserting both values resolve. Backed by new ScalarInputData / ScalarInputStep test assets.

Test framework (.NET 10 TFMs)

  • Added net10.0 to test/Directory.Build.props, WorkflowCore.UnitTests.csproj, and WorkflowCore.IntegrationTests.csproj.

CI

  • Install the .NET 10 SDK in the Azure-Tests job. That job runs a repo-wide dotnet restore, which failed with NETSDK1045 once the test projects began targeting net10.0; it now matches the other jobs (6/8/9/10).
  • Merged master to bring the branch current (it was behind the Azure Table Storage provider work).

Testing

  • 59/59 unit tests pass on .NET 10, including the new Support for .NET 10 / planned release timeline #1428 regression test.
  • CI: all jobs green except Elasticsearch-Tests, which is a pre-existing infrastructure flake (Elasticsearch container health-check timeout) that fails identically on master and is unrelated to this change.

Note on the reported crash

The InvalidProgramException originates in the JIT (RuntimeHelpers.CompileMethod). I was unable to reproduce it on any GA/servicing .NET 10 build (10.0.0-preview.1/.4/.6/.7, 10.0.0 GA, 10.0.10) across arm64 and x64 — so it appears tied to a specific runtime build/architecture. This change is still the correct fix: it removes the fragile Compile()-inside-closure pattern and eliminates per-invocation recompilation (a real perf win).

…ation

Pre-compile dynamic expressions outside closures/local functions in
DefinitionLoader to avoid InvalidProgramException on .NET 10's updated
expression tree compiler. The previous pattern of calling
LambdaExpression.Compile() inside closures that capture expression
objects produces invalid IL on .NET 10.

Changes:
- BuildScalarInputAction: compile expression before local function
- BuildObjectInputAction: pre-scan JObject and compile all @-prefixed
  expressions at definition load time instead of at each invocation
- AttachDirectlyOutput: pre-compile source expression before lambda
- AttachNestedOutput: pre-compile both target and source expressions
- MemberMapParameter: cache compiled source delegate in constructor
- Add net10.0 to test TFMs (Directory.Build.props, UnitTests, IntegrationTests)

Fixes #1428

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a .NET 10 InvalidProgramException observed during dynamic expression compilation by moving LambdaExpression.Compile() calls out of closures/local functions and caching compiled delegates to avoid problematic capture patterns in the updated .NET 10 expression tree compiler.

Changes:

  • Pre-compile and cache delegates for scalar/object input expressions and output mappings in DefinitionLoader.
  • Cache the compiled source delegate in MemberMapParameter to avoid recompiling on each assignment.
  • Add net10.0 as a test target framework across test props and key test projects.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj Multi-target unit tests on net6/net8/net10.
test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj Multi-target integration tests on net6/net8/net10.
test/Directory.Build.props Adds net10.0 to default test TFMs.
src/WorkflowCore/Models/MemberMapParameter.cs Caches compiled source delegate to avoid repeated compilation.
src/WorkflowCore.DSL/Services/DefinitionLoader.cs Pre-compiles expressions outside closures and caches compiled delegates for inputs/outputs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +545 to +546
foreach (var child in subobj.Children<JObject>())
scanStack.Push(child);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, but this is intentional here: the pre-scan deliberately mirrors the exact same Children<JObject>() traversal the runtime acn uses below, so the pre-compiled compiledExpressions dictionary is guaranteed to contain a key for every @-property the runtime scan will later look up — no KeyNotFound risk. The nested-object limitation you spotted is pre-existing behavior on master (this PR only lifts the compile step out of the closure to fix the .NET 10 InvalidProgramException); making the template walk recurse into nested objects is a separate behavior change I would rather not fold into this fix.

Comment on lines 560 to 564
if (prop.Name.StartsWith("@"))
{
var sourceExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), TransformExpression(prop.Value.ToString()));
var exprText = prop.Value.ToString();
object resolvedValue;
try

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Children<JObject>() line is unchanged from master — it is the existing runtime traversal, not something introduced by this PR. Scope here is limited to the .NET 10 compile fix, so I am keeping the traversal behavior identical. Happy to open a follow-up to properly recurse nested objects/array elements (in both the pre-scan and this loop together) if we want to support nested @ expressions.

danielgerlag and others added 3 commits July 15, 2026 09:05
Covers the reporter's exact scenario: a scalar variable-binding input
(data.MessageId) plus a scalar string-literal input ("waits-for-batching").
Loads the definition and assigns the inputs the same way
WorkflowExecutor.ExecuteStep does, asserting both values resolve.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Azure-Tests job runs a repo-wide 'dotnet restore', which now fails
with NETSDK1045 because the test projects target net10.0 but this job
only installed the .NET 8/9 SDKs. Align it with the other jobs (6/8/9/10).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for .NET 10 / planned release timeline

2 participants