From 4d60ab427fb9975cd40c6f7b0d39d4c0f4d71929 Mon Sep 17 00:00:00 2001 From: Daniel Gerlag Date: Fri, 15 May 2026 17:05:57 -0700 Subject: [PATCH] Add .NET 10 support: fix InvalidProgramException in expression compilation 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> --- .../Services/DefinitionLoader.cs | 55 ++++++++++++++++--- src/WorkflowCore/Models/MemberMapParameter.cs | 20 ++++--- test/Directory.Build.props | 2 +- .../WorkflowCore.IntegrationTests.csproj | 2 +- .../WorkflowCore.UnitTests.csproj | 2 +- 5 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs index 5d6595550..29465dbf9 100644 --- a/src/WorkflowCore.DSL/Services/DefinitionLoader.cs +++ b/src/WorkflowCore.DSL/Services/DefinitionLoader.cs @@ -318,12 +318,14 @@ private void AttachDirectlyOutput(KeyValuePair output, WorkflowS propertyInfo = dataType.GetProperty("Item"); targetProperty = Expression.Property(dataParameter, propertyInfo, Expression.Constant(output.Key)); + var compiledSourceExpr = sourceExpr.Compile(); + Action acn = (pStep, pData) => { object resolvedValue; try { - resolvedValue = sourceExpr.Compile().DynamicInvoke(pStep); + resolvedValue = compiledSourceExpr.DynamicInvoke(pStep); } catch (TargetInvocationException ex) { @@ -377,13 +379,16 @@ private void AttachNestedOutput(KeyValuePair output, WorkflowSte } propertyInfo = ((PropertyInfo)memberExpression.Member).PropertyType.GetProperty("Item"); + var targetExpr = Expression.Lambda(memberExpression, dataParameter); + var compiledTargetExpr = targetExpr.Compile(); + var compiledSourceExpr = sourceExpr.Compile(); + Action acn = (pStep, pData) => { - var targetExpr = Expression.Lambda(memberExpression, dataParameter); object data; try { - data = targetExpr.Compile().DynamicInvoke(pData); + data = compiledTargetExpr.DynamicInvoke(pData); } catch (TargetInvocationException ex) { @@ -392,7 +397,7 @@ private void AttachNestedOutput(KeyValuePair output, WorkflowSte object resolvedValue; try { - resolvedValue = sourceExpr.Compile().DynamicInvoke(pStep); + resolvedValue = compiledSourceExpr.DynamicInvoke(pStep); } catch (TargetInvocationException ex) { @@ -470,12 +475,14 @@ private static Action BuildScalarInput throw new WorkflowDefinitionLoadException($"Error parsing input expression '{expr}' for property '{input.Key}': {ex.Message}", ex); } + var compiledExpr = sourceExpr.Compile(); + void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) { object resolvedValue; try { - resolvedValue = sourceExpr.Compile().DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); + resolvedValue = compiledExpr.DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); } catch (TargetInvocationException ex) { @@ -505,6 +512,40 @@ void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) private static Action BuildObjectInputAction(KeyValuePair input, ParameterExpression dataParameter, ParameterExpression contextParameter, ParameterExpression environmentVarsParameter, PropertyInfo stepProperty) { + // Pre-compile all @-prefixed property expressions at definition load time + var compiledExpressions = new Dictionary(); + var templateObj = JObject.FromObject(input.Value); + var scanStack = new Stack(); + scanStack.Push(templateObj); + + while (scanStack.Count > 0) + { + var subobj = scanStack.Pop(); + foreach (var prop in subobj.Properties()) + { + if (prop.Name.StartsWith("@")) + { + var exprText = prop.Value.ToString(); + if (!compiledExpressions.ContainsKey(exprText)) + { + LambdaExpression sourceExpr; + try + { + sourceExpr = DynamicExpressionParser.ParseLambda(ParsingConfig, false, new[] { dataParameter, contextParameter, environmentVarsParameter }, typeof(object), TransformExpression(exprText)); + } + catch (Exception ex) when (ex is System.Linq.Dynamic.Core.Exceptions.ParseException || ex is InvalidOperationException) + { + throw new WorkflowDefinitionLoadException($"Error parsing input expression '{exprText}': {ex.Message}", ex); + } + compiledExpressions[exprText] = sourceExpr.Compile(); + } + } + } + + foreach (var child in subobj.Children()) + scanStack.Push(child); + } + void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) { var stack = new Stack(); @@ -518,11 +559,11 @@ void acn(IStepBody pStep, object pData, IStepExecutionContext pContext) { 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 { - resolvedValue = sourceExpr.Compile().DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); + resolvedValue = compiledExpressions[exprText].DynamicInvoke(pData, pContext, Environment.GetEnvironmentVariables()); } catch (TargetInvocationException ex) { diff --git a/src/WorkflowCore/Models/MemberMapParameter.cs b/src/WorkflowCore/Models/MemberMapParameter.cs index e5273986d..13ae0f38e 100644 --- a/src/WorkflowCore/Models/MemberMapParameter.cs +++ b/src/WorkflowCore/Models/MemberMapParameter.cs @@ -9,6 +9,7 @@ public class MemberMapParameter : IStepParameter { private readonly LambdaExpression _source; private readonly LambdaExpression _target; + private readonly Delegate _compiledSource; public MemberMapParameter(LambdaExpression source, LambdaExpression target) { @@ -17,19 +18,20 @@ public MemberMapParameter(LambdaExpression source, LambdaExpression target) _source = source; _target = target; + _compiledSource = source.Compile(); } - private void Assign(object sourceObject, LambdaExpression sourceExpr, object targetObject, LambdaExpression targetExpr, IStepExecutionContext context) + private void Assign(object sourceObject, object targetObject, IStepExecutionContext context) { object resolvedValue = null; - switch (sourceExpr.Parameters.Count) + switch (_source.Parameters.Count) { case 1: - resolvedValue = sourceExpr.Compile().DynamicInvoke(sourceObject); + resolvedValue = _compiledSource.DynamicInvoke(sourceObject); break; case 2: - resolvedValue = sourceExpr.Compile().DynamicInvoke(sourceObject, context); + resolvedValue = _compiledSource.DynamicInvoke(sourceObject, context); break; default: throw new ArgumentException(); @@ -37,24 +39,24 @@ private void Assign(object sourceObject, LambdaExpression sourceExpr, object tar if (resolvedValue == null) { - var defaultAssign = Expression.Lambda(Expression.Assign(targetExpr.Body, Expression.Default(targetExpr.ReturnType)), targetExpr.Parameters.Single()); + var defaultAssign = Expression.Lambda(Expression.Assign(_target.Body, Expression.Default(_target.ReturnType)), _target.Parameters.Single()); defaultAssign.Compile().DynamicInvoke(targetObject); return; } - var valueExpr = Expression.Convert(Expression.Constant(resolvedValue), targetExpr.ReturnType); - var assign = Expression.Lambda(Expression.Assign(targetExpr.Body, valueExpr), targetExpr.Parameters.Single()); + var valueExpr = Expression.Convert(Expression.Constant(resolvedValue), _target.ReturnType); + var assign = Expression.Lambda(Expression.Assign(_target.Body, valueExpr), _target.Parameters.Single()); assign.Compile().DynamicInvoke(targetObject); } public void AssignInput(object data, IStepBody body, IStepExecutionContext context) { - Assign(data, _source, body, _target, context); + Assign(data, body, context); } public void AssignOutput(object data, IStepBody body, IStepExecutionContext context) { - Assign(body, _source, data, _target, context); + Assign(body, data, context); } } } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index cd70cf018..1947c22b5 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,6 +1,6 @@ - net6.0;net8.0 + net6.0;net8.0;net10.0 latest false diff --git a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj index c92182936..996a9831e 100644 --- a/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj +++ b/test/WorkflowCore.IntegrationTests/WorkflowCore.IntegrationTests.csproj @@ -7,7 +7,7 @@ false false false - net6.0 + net6.0;net8.0;net10.0 diff --git a/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj b/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj index ff8f2e19e..4822cced6 100644 --- a/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj +++ b/test/WorkflowCore.UnitTests/WorkflowCore.UnitTests.csproj @@ -7,7 +7,7 @@ false false false - net6.0 + net6.0;net8.0;net10.0