Fix naming collision between workflow input and activity input

This commit is contained in:
Sipke Schoorstra 2023-09-06 13:50:08 +02:00
parent 6eda27b034
commit 69744bd63c
7 changed files with 522 additions and 6 deletions

View file

@ -53,10 +53,11 @@ public static class ServiceProviderExtensions
/// </summary>
/// <param name="services">The services.</param>
/// <param name="workflowDefinitionId">The ID of the workflow definition.</param>
/// <param name="input">An optional dictionary of input values.</param>
/// <returns>The workflow state.</returns>
public static async Task<WorkflowState> RunWorkflowUntilEndAsync(this IServiceProvider services, string workflowDefinitionId)
public static async Task<WorkflowState> RunWorkflowUntilEndAsync(this IServiceProvider services, string workflowDefinitionId, IDictionary<string, object>? input = default)
{
var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, new Dictionary<string, object>(), VersionOptions.Published);
var startWorkflowOptions = new StartWorkflowRuntimeOptions(null, input, VersionOptions.Published);
var workflowRuntime = services.GetRequiredService<IWorkflowRuntime>();
var result = await workflowRuntime.StartWorkflowAsync(workflowDefinitionId, startWorkflowOptions);
var bookmarks = new Stack<Bookmark>(result.Bookmarks);
@ -67,7 +68,7 @@ public static class ServiceProviderExtensions
var resumeOptions = new ResumeWorkflowRuntimeOptions(BookmarkId: bookmark.Id);
var resumeResult = await workflowRuntime.ResumeWorkflowAsync(result.WorkflowInstanceId, resumeOptions);
foreach (var newBookmark in resumeResult.Bookmarks)
foreach (var newBookmark in resumeResult!.Bookmarks)
bookmarks.Push(newBookmark);
}

View file

@ -9,6 +9,7 @@ using Elsa.JavaScript.Extensions;
using Elsa.JavaScript.Notifications;
using Elsa.JavaScript.Options;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Core.Activities;
using Elsa.Workflows.Core.Memory;
using Humanizer;
using Jint;
@ -67,12 +68,14 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
engine.SetValue("getInput", (Func<string, object?>)(name => context.GetWorkflowExecutionContext().Input.GetValue(name)));
engine.SetValue("getOutputFrom", (Func<string, string?, object?>)((activityIdOrNodeId, outputName) => GetOutput(context, activityIdOrNodeId, outputName)));
engine.SetValue("getLastResult", (Func<object?>)(() => GetLastResult(context)));
// Create workflow input accessors.
CreateWorkflowInputAccessors(engine, context);
// Create variable getters and setters for each variable.
CreateVariableAccessors(engine, context);
// Create workflow input accessors - only if the current activity is not part of a composite activity definition.
// Otherwise, the workflow input accessors will hide the composite activity input accessors which rely on variable accessors created above.
if(!IsInsideCompositeActivity(context))
CreateWorkflowInputAccessors(engine, context);
// Create output getters for each activity.
CreateOutputAccessors(engine, context);
@ -103,6 +106,17 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
return engine;
}
private static bool IsInsideCompositeActivity(ExpressionExecutionContext context)
{
if(!context.TryGetActivityExecutionContext(out var activityExecutionContext))
return false;
// If the first workflow definition in the ancestor hierarchy and that workflow definition has a parent, then we are inside a composite activity.
var firstWorkflowContext = activityExecutionContext.GetAncestors().FirstOrDefault(x => x.Activity is Workflow);
return firstWorkflowContext?.ParentActivityExecutionContext != null;
}
private static object? GetLastResult(ExpressionExecutionContext context)
{
var workflowExecutionContext = context.GetWorkflowExecutionContext();
@ -123,6 +137,11 @@ public class JintJavaScriptEvaluator : IJavaScriptEvaluator
private void CreateWorkflowInputAccessors(Engine engine, ExpressionExecutionContext context)
{
if(!context.TryGetActivityExecutionContext(out var activityExecutionContext))
return;
// Only create workflow input accessors if the current activity is not part of a composite activity definition.
if(context.TryGetWorkflowExecutionContext(out var workflowExecutionContext))
{
var input = workflowExecutionContext.Input;

View file

@ -87,8 +87,18 @@
<None Update="Scenarios\FlowchartCompletion\Workflows\workflow5.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\ParentChildInputs\Workflows\child.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\ParentChildInputs\Workflows\parent1.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Scenarios\ParentChildInputs\Workflows\parent2.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -0,0 +1,71 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Elsa.Testing.Shared;
using Elsa.Workflows.Core;
using Xunit;
using Xunit.Abstractions;
namespace Elsa.IntegrationTests.Scenarios.ParentChildInputs;
/// <summary>
/// Tests for the flowchart completion feature using various workflow setups.
/// </summary>
public class Tests
{
private readonly CapturingTextWriter _capturingTextWriter = new();
private readonly IServiceProvider _services;
public Tests(ITestOutputHelper testOutputHelper)
{
_services = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build();
}
[Fact(DisplayName = "Child activity receives workflow input from parent event if same name.")]
public async Task Test1()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import child workflow.
var childFileName = $"Scenarios/ParentChildInputs/Workflows/child.json";
await _services.ImportWorkflowDefinitionAsync(childFileName);
// Import parent workflow.
var parentFileName = $"Scenarios/ParentChildInputs/Workflows/parent1.json";
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(parentFileName);
// Execute.
var input = new Dictionary<string, object> { ["Input1"] = "Foo" };
await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId, input);
// Assert expected output.
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Parent: Foo", "Child: Bar" }, lines);
}
[Fact(DisplayName = "Child activity receives input from parent event if same name and does not use global workflow input.")]
public async Task Test2()
{
// Populate registries.
await _services.PopulateRegistriesAsync();
// Import child workflow.
var childFileName = $"Scenarios/ParentChildInputs/Workflows/child.json";
await _services.ImportWorkflowDefinitionAsync(childFileName);
// Import parent workflow.
var parentFileName = $"Scenarios/ParentChildInputs/Workflows/parent2.json";
var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(parentFileName);
// Execute.
var input = new Dictionary<string, object> { ["Input1"] = "Foo" };
await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId, input);
// Assert expected output.
var lines = _capturingTextWriter.Lines.ToList();
Assert.Equal(new[] { "Parent: Foo", "Child: Foo" }, lines);
}
}

View file

@ -0,0 +1,139 @@
{
"id": "228c0150ab7b4745b038b950be574935",
"definitionId": "25b097966de245818e9a5c0d04f3323e",
"name": "Child",
"createdAt": "2023-09-06T09:57:32.944622+00:00",
"version": 5,
"variables": [],
"inputs": [
{
"uiHint": "single-line",
"storageDriverType": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core",
"type": "Object",
"name": "Input1",
"displayName": "Input1",
"description": "",
"category": "",
"isArray": false
}
],
"outputs": [
{
"type": "Object",
"name": "Output1",
"displayName": "Output1",
"description": "",
"isArray": false
}
],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isLatest": true,
"isPublished": true,
"options": {
"usableAsActivity": true,
"autoUpdateConsumingWorkflows": true
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "Flowchart1",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"NotFoundConnectionsKey": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"start": "WriteLine1",
"activities": [
{
"outputName": {
"typeName": "String",
"expression": {
"type": "Literal",
"value": "Output1"
},
"memoryReference": {
"id": "ca8473b5-83ec-4f75-a2f1-dfba71f349d1"
}
},
"outputValue": {
"typeName": "Object",
"expression": {
"type": "JavaScript",
"value": "return getInput1();"
},
"memoryReference": {
"id": "06c6243d-8a57-45e3-ae54-f209b95d2bb2"
}
},
"id": "SetOutput1",
"name": null,
"type": "Elsa.SetOutput",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 636,
"y": 250.01388549804688
},
"size": {
"width": 107.734375,
"height": 50
}
}
}
},
{
"text": {
"typeName": "String",
"expression": {
"type": "JavaScript",
"value": "\u0022Child: \u0022 \u002B getInput1();"
},
"memoryReference": {
"id": "3f726554-0fa1-4e54-b5e7-74b18807437b"
}
},
"id": "WriteLine1",
"name": null,
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 332.01385498046875,
"y": 222.01388549804688
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
}
],
"connections": [
{
"source": {
"activity": "WriteLine1",
"port": "Done"
},
"target": {
"activity": "SetOutput1",
"port": "In"
}
}
]
}
}

View file

@ -0,0 +1,138 @@
{
"id": "e49f81715e29460187671ee298bbd0b2",
"definitionId": "6a4f5aeecbf94fc09dbba65363c5cb57",
"name": "Parent1",
"createdAt": "2023-09-06T11:38:10.701838+00:00",
"version": 8,
"variables": [],
"inputs": [
{
"uiHint": "single-line",
"storageDriverType": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core",
"type": "Object",
"name": "Input1",
"displayName": "Input1",
"description": "",
"category": "",
"isArray": false
}
],
"outputs": [
{
"type": "Object",
"name": "Output1",
"displayName": "Output1",
"description": "",
"isArray": false
}
],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isLatest": true,
"isPublished": false,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "Flowchart1",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"NotFoundConnectionsKey": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"start": "WriteLine1",
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "JavaScript",
"value": "\u0022Parent: \u0022 \u002B getInput1();"
},
"memoryReference": {
"id": "3070bee4-8309-44e3-a805-a9ec7fdd7ac2"
}
},
"id": "WriteLine1",
"name": null,
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 160,
"y": 280
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"workflowDefinitionId": "25b097966de245818e9a5c0d04f3323e",
"workflowDefinitionVersionId": "228c0150ab7b4745b038b950be574935",
"latestAvailablePublishedVersion": 5,
"latestAvailablePublishedVersionId": "228c0150ab7b4745b038b950be574935",
"id": "22jW7xiYM0K30H99EDWztQ",
"name": "Child1",
"type": "Child",
"version": 5,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 405.3984375,
"y": 204.0078125
},
"size": {
"width": 94.765625,
"height": 116.015625
}
}
},
"input1": {
"typeName": "Object",
"expression": {
"type": "JavaScript",
"value": "\u0060Bar\u0060"
},
"memoryReference": {
"id": "22jW7xiYM0K30H99EDWztQ:input-0"
}
},
"output1": {
"typeName": "Object",
"memoryReference": {
"id": "Output1"
}
}
}
],
"connections": [
{
"source": {
"activity": "WriteLine1",
"port": "Done"
},
"target": {
"activity": "22jW7xiYM0K30H99EDWztQ",
"port": "In"
}
}
]
}
}

View file

@ -0,0 +1,138 @@
{
"id": "c5aa339e124f4486b14dbfdc133a45d1",
"definitionId": "8ad20b5ff9ee4ff59b8dfcfb6d4e29a0",
"name": "Parent2",
"createdAt": "2023-09-06T11:38:30.217265+00:00",
"version": 1,
"variables": [],
"inputs": [
{
"uiHint": "single-line",
"storageDriverType": "Elsa.Workflows.Core.Services.WorkflowStorageDriver, Elsa.Workflows.Core",
"type": "Object",
"name": "Input1",
"displayName": "Input1",
"description": "",
"category": "",
"isArray": false
}
],
"outputs": [
{
"type": "Object",
"name": "Output1",
"displayName": "Output1",
"description": "",
"isArray": false
}
],
"outcomes": [],
"customProperties": {},
"isReadonly": false,
"isLatest": true,
"isPublished": true,
"options": {
"autoUpdateConsumingWorkflows": false
},
"root": {
"type": "Elsa.Flowchart",
"version": 1,
"id": "Flowchart1",
"metadata": {},
"customProperties": {
"source": "FlowchartJsonConverter.cs:45",
"NotFoundConnectionsKey": [],
"canStartWorkflow": false,
"runAsynchronously": false
},
"start": "WriteLine1",
"activities": [
{
"text": {
"typeName": "String",
"expression": {
"type": "JavaScript",
"value": "\u0022Parent: \u0022 \u002B getInput1();"
},
"memoryReference": {
"id": "3070bee4-8309-44e3-a805-a9ec7fdd7ac2"
}
},
"id": "WriteLine1",
"name": null,
"type": "Elsa.WriteLine",
"version": 1,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 160,
"y": 280
},
"size": {
"width": 139.296875,
"height": 50
}
}
}
},
{
"workflowDefinitionId": "25b097966de245818e9a5c0d04f3323e",
"workflowDefinitionVersionId": "228c0150ab7b4745b038b950be574935",
"latestAvailablePublishedVersion": 5,
"latestAvailablePublishedVersionId": "228c0150ab7b4745b038b950be574935",
"id": "22jW7xiYM0K30H99EDWztQ",
"name": "Child1",
"type": "Child",
"version": 5,
"customProperties": {
"canStartWorkflow": false,
"runAsynchronously": false
},
"metadata": {
"designer": {
"position": {
"x": 405.3984375,
"y": 204.0078125
},
"size": {
"width": 135.390625,
"height": 120
}
}
},
"input1": {
"typeName": "Object",
"expression": {
"type": "JavaScript",
"value": "getInput1()"
},
"memoryReference": {
"id": "22jW7xiYM0K30H99EDWztQ:input-0"
}
},
"output1": {
"typeName": "Object",
"memoryReference": {
"id": "Output1"
}
}
}
],
"connections": [
{
"source": {
"activity": "WriteLine1",
"port": "Done"
},
"target": {
"activity": "22jW7xiYM0K30H99EDWztQ",
"port": "In"
}
}
]
}
}