Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 55 additions & 42 deletions src/ConductorSharp.Engine/Util/ExpressionUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,59 @@ public static Type ParseToType(Expression expression)
return ((PropertyInfo)taskSelectExpression.Member).PropertyType;
}

public static JObject ParseToParameters(Expression expression)
public static JObject ParseToParameters(Expression expression) => ParseObjectInitialization(expression);

private static object ParseExpression(Expression expression)
{
if (expression is ConstantExpression cex)
return Convert.ToString(cex.Value);

if (expression is UnaryExpression uex && uex.Operand is ConstantExpression ccex)
{
var converted = Convert.ToString(ccex.Value);

//if (ccex.Value is bool)
// return converted.ToLowerInvariant();

return ccex.Value;
}

// Handle boxing
if (expression is UnaryExpression unaryEx && unaryEx.NodeType == ExpressionType.Convert)
return CreateExpressionString(unaryEx.Operand);

if (
expression is MethodCallExpression methodExpression
&& methodExpression.Method.Name == nameof(string.Format)
&& methodExpression.Method.DeclaringType == typeof(string)
)
{
var expressionStrings = methodExpression.Arguments.Skip(1).Select(CompileMemberOrNameExpressions).ToArray();
var formatExpr = methodExpression.Arguments[0] as ConstantExpression;
if (formatExpr == null)
throw new Exception("string.Format with non constant format string is not supported");
var formatString = (string)formatExpr.Value;
return string.Format(formatString, expressionStrings);
}

if (expression is NewExpression || expression is MemberInitExpression)
return ParseObjectInitialization(expression);

if (expression is NewArrayExpression newArrayExpression)
return ParseArrayInitalization(newArrayExpression);

return CompileMemberOrNameExpressions(expression);
}

private static JArray ParseArrayInitalization(NewArrayExpression newArrayExpression)
{
if (newArrayExpression.NodeType != ExpressionType.NewArrayInit)
throw new Exception("Only dimensionless array initialization is supported");

return new JArray(newArrayExpression.Expressions.Select(ParseExpression));
Comment on lines +76 to +81

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems to return [ 1,2,3] as ["1", "2", "3"]. The reason is

 if (expression is ConstantExpression cex)
                return Convert.ToString(cex.Value);

@boma96
We can extract the correct type, but this might break something somewhere. However, most frameworks seem to be equipped to handle string to int conversion automatically if it is really convertible. I think we should do this, what do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It will also break our tests

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thank you for pointing this out, did not notice it

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ok, will make separate PR for this

}

private static JObject ParseObjectInitialization(Expression expression)
{
var inputParams = new JObject();

Expand All @@ -41,7 +93,7 @@ public static JObject ParseToParameters(Expression expression)
throw new Exception($"Only {nameof(MemberBindingType.Assignment)} binding type supported");

var assignmentBinding = (MemberAssignment)binding;
var assignmentValue = ParseToAssignmentString(assignmentBinding.Expression);
var assignmentValue = ParseExpression(assignmentBinding.Expression);
var assignmentKey = GetMemberName(binding.Member as PropertyInfo);

inputParams.Add(new JProperty(assignmentKey, assignmentValue));
Expand All @@ -62,7 +114,7 @@ var member in newExpression.Arguments.Zip(
)
)
{
var assignmentValue = ParseToAssignmentString(member.expression);
var assignmentValue = ParseExpression(member.expression);
var assignmentKey = GetMemberName(member.memberInfo as PropertyInfo);

inputParams.Add(new JProperty(assignmentKey, assignmentValue));
Expand All @@ -76,45 +128,6 @@ var member in newExpression.Arguments.Zip(
return inputParams;
}

private static object ParseToAssignmentString(Expression assignmentExpression)
{
if (assignmentExpression is ConstantExpression cex)
return Convert.ToString(cex.Value);

if (assignmentExpression is UnaryExpression uex && uex.Operand is ConstantExpression ccex)
{
var converted = Convert.ToString(ccex.Value);

//if (ccex.Value is bool)
// return converted.ToLowerInvariant();

return ccex.Value;
}

// Handle boxing
if (assignmentExpression is UnaryExpression unaryEx && unaryEx.NodeType == ExpressionType.Convert)
return CreateExpressionString(unaryEx.Operand);

if (
assignmentExpression is MethodCallExpression methodExpression
&& methodExpression.Method.Name == nameof(string.Format)
&& methodExpression.Method.DeclaringType == typeof(string)
)
{
var expressionStrings = methodExpression.Arguments.Skip(1).Select(CompileMemberOrNameExpressions).ToArray();
var formatExpr = methodExpression.Arguments[0] as ConstantExpression;
if (formatExpr == null)
throw new Exception("string.Format with non constant format string is not supported");
var formatString = (string)formatExpr.Value;
return string.Format(formatString, expressionStrings);
}

if (assignmentExpression is NewExpression || assignmentExpression is MemberInitExpression)
return ParseToParameters(assignmentExpression);

return CompileMemberOrNameExpressions(assignmentExpression);
}

private static string CompileMemberOrNameExpressions(Expression expr)
{
if (expr is MemberExpression)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ItemGroup>
<None Remove="Samples\Tasks\CustomerGet.json" />
<None Remove="Samples\Tasks\EmailPrepare.json" />
<None Remove="Samples\Workflows\Arrays.json" />
<None Remove="Samples\Workflows\NestedObjects.json" />
<None Remove="Samples\Workflows\SendCustomerNotification.json" />
<None Remove="Samples\Workflows\StringInterpolation.json" />
Expand All @@ -17,6 +18,7 @@
<ItemGroup>
<EmbeddedResource Include="Samples\Tasks\CustomerGet.json" />
<EmbeddedResource Include="Samples\Tasks\EmailPrepare.json" />
<EmbeddedResource Include="Samples\Workflows\Arrays.json" />
<EmbeddedResource Include="Samples\Workflows\NestedObjects.json" />
<EmbeddedResource Include="Samples\Workflows\SendCustomerNotification.json" />
<EmbeddedResource Include="Samples\Workflows\StringInterpolation.json" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,18 @@ public void BuilderReturnsCorrectDefinitionNestedObjects()
public void BuildersReturnSameDefinitionsTaskInitialization()
{
var explicitDef = SerializationUtil.SerializeObject(new TaskInputInitializationNew().GetDefinition());
var targetTypedDef = SerializationUtil.SerializeObject(new TaskInputInitializationMemberInit().GetDefinition());
var memberInitDef = SerializationUtil.SerializeObject(new TaskInputInitializationMemberInit().GetDefinition());

Assert.True(explicitDef == targetTypedDef, "Definitions are not equal");
Assert.True(explicitDef == memberInitDef, "Definitions are not equal");
}

[Fact]
public void BuilderReturnsCorrectDefinitionArrayTask()
{
var definition = SerializationUtil.SerializeObject(new Arrays().GetDefinition());
var expectedDefinition = EmbeddedFileHelper.GetLinesFromEmbeddedFile("~/Samples/Workflows/Arrays.json");

Assert.Equal(expectedDefinition, definition);
}
}
}
24 changes: 24 additions & 0 deletions test/ConductorSharp.Engine.Tests/Samples/Tasks/ArrayTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConductorSharp.Engine.Tests.Samples.Tasks
{
public class ArrayTaskInput : IRequest<ArrayTaskOutput>
{
public class TestModel
{
public string String { get; set; }
}

public int[] Integers { get; set; }
public TestModel[] Models { get; set; }
public object Objects { get; set; }
}

public class ArrayTaskOutput { }

public class ArrayTask : SimpleTaskModel<ArrayTaskInput, ArrayTaskOutput> { }
}
51 changes: 51 additions & 0 deletions test/ConductorSharp.Engine.Tests/Samples/Workflows/Arrays.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConductorSharp.Engine.Tests.Samples.Workflows
{
public class ArrayInput : WorkflowInput<ArrayOutput> { }

public class ArrayOutput : WorkflowOutput { }

public class Arrays : Workflow<ArrayInput, ArrayOutput>
{
public ArrayTask ArrayTask1 { get; set; }
public ArrayTask ArrayTask2 { get; set; }

public override WorkflowDefinition GetDefinition()
{
var builder = new WorkflowDefinitionBuilder<Arrays>();

builder.AddTask(
wf => wf.ArrayTask1,
wf =>
new()
{
Integers = new[] { 1, 2, 3 },
Models = new[]
{
new ArrayTaskInput.TestModel { String = "Test1" },
new ArrayTaskInput.TestModel { String = "Test2" }
},
Objects = new dynamic[] { new { AnonymousObjProp = "Prop" }, new { Test = "Prop" } }
}
);

builder.AddTask(
wf => wf.ArrayTask2,
wf =>
new()
{
Integers = new int[] { },
Models = new ArrayTaskInput.TestModel[] { },
Objects = new dynamic[] { }
}
);

return builder.Build(opts => opts.Version = 1);
}
}
}
112 changes: 112 additions & 0 deletions test/ConductorSharp.Engine.Tests/Samples/Workflows/Arrays.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
{
"ownerApp": null,
"createTime": 0,
"updateTime": 0,
"createdBy": null,
"updatedBy": null,
"name": "arrays",
"description": "{\"description\":null,\"labels\":null}",
"version": 1,
"tasks": [
{
"queryExpression": null,
"name": "array_task",
"taskReferenceName": "array_task1",
"description": "{\"description\":null}",
"inputParameters": {
"integers": [
"1",
"2",
"3"
],
"models": [
{
"string": "Test1"
},
{
"string": "Test2"
}
],
"objects": [
{
"anonymous_obj_prop": "Prop"
},
{
"test": "Prop"
}
]
},
"type": "SIMPLE",
"dynamicTaskNameParam": null,
"caseValueParam": null,
"caseExpression": null,
"expression": null,
"evaluatorType": null,
"scriptExpression": null,
"decisionCases": null,
"dynamicForkJoinTasksParam": null,
"dynamicForkTasksParam": null,
"dynamicForkTasksInputParamName": null,
"defaultCase": null,
"forkTasks": null,
"startDelay": 0,
"subWorkflowParam": null,
"joinOn": null,
"sink": null,
"optional": false,
"taskDefinition": null,
"rateLimited": false,
"defaultExclusiveJoinTask": null,
"asyncComplete": false,
"loopCondition": null,
"loopOver": null
},
{
"queryExpression": null,
"name": "array_task",
"taskReferenceName": "array_task2",
"description": "{\"description\":null}",
"inputParameters": {
"integers": [],
"models": [],
"objects": []
},
"type": "SIMPLE",
"dynamicTaskNameParam": null,
"caseValueParam": null,
"caseExpression": null,
"expression": null,
"evaluatorType": null,
"scriptExpression": null,
"decisionCases": null,
"dynamicForkJoinTasksParam": null,
"dynamicForkTasksParam": null,
"dynamicForkTasksInputParamName": null,
"defaultCase": null,
"forkTasks": null,
"startDelay": 0,
"subWorkflowParam": null,
"joinOn": null,
"sink": null,
"optional": false,
"taskDefinition": null,
"rateLimited": false,
"defaultExclusiveJoinTask": null,
"asyncComplete": false,
"loopCondition": null,
"loopOver": null
}
],
"inputParameters": [
"{}"
],
"outputParameters": null,
"failureWorkflow": null,
"schemaVersion": 2,
"restartable": true,
"workflowStatusListenerEnabled": true,
"ownerEmail": null,
"timeoutPolicy": null,
"timeoutSeconds": 0,
"variables": null
}