diff --git a/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj b/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj index afab452e4..d181e8522 100644 --- a/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj +++ b/src/common/Elsa.Testing.Shared/Elsa.Testing.Shared.csproj @@ -17,6 +17,7 @@ + diff --git a/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs b/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs index 7b17247c9..58894001f 100644 --- a/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs +++ b/src/common/Elsa.Testing.Shared/TestApplicationBuilder.cs @@ -37,6 +37,7 @@ public class TestApplicationBuilder _configureElsa += elsa => elsa .AddActivitiesFrom() .UseScheduling() + .UseCSharp() .UseJavaScript() .UseLiquid() .UseDsl() diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index 6c80b0df6..2ca6842f5 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -21,16 +21,17 @@ public static class ExpressionExecutionContextExtensions /// The key used to store the in the dictionary. /// public static readonly object WorkflowExecutionContextKey = new(); - + /// /// The key used to store the in the dictionary. /// public static readonly object ActivityExecutionContextKey = new(); - + /// /// The /// public static readonly object InputKey = new(); + public static readonly object WorkflowKey = new(); /// @@ -64,14 +65,14 @@ public static class ExpressionExecutionContextExtensions /// Returns the of the specified /// public static WorkflowExecutionContext GetWorkflowExecutionContext(this ExpressionExecutionContext context) => (WorkflowExecutionContext)context.TransientProperties[WorkflowExecutionContextKey]; - + /// /// Returns the of the specified /// /// /// public static ActivityExecutionContext GetActivityExecutionContext(this ExpressionExecutionContext context) => (ActivityExecutionContext)context.TransientProperties[ActivityExecutionContextKey]; - + /// /// Returns the of the specified /// @@ -84,18 +85,18 @@ public static class ExpressionExecutionContextExtensions /// Returns the value of the specified input. /// public static T? Get(this ExpressionExecutionContext context, Input? input) => input != null ? context.GetBlock(input.MemoryBlockReference).Value.ConvertTo() : default; - + /// /// Returns the value of the specified output. /// public static T? Get(this ExpressionExecutionContext context, Output output) => context.GetBlock(output.MemoryBlockReference).Value.ConvertTo(); - + /// /// Returns the value of the specified output. /// public static object? Get(this ExpressionExecutionContext context, Output output) => context.GetBlock(output.MemoryBlockReference).Value; - - + + /// /// Returns the value of the variable with the specified name. /// @@ -112,7 +113,7 @@ public static class ExpressionExecutionContextExtensions if (metadata!.Variable.Name == name) return metadata.Variable; } - + return localScopeOnly ? null : context.ParentContext?.GetVariable(name); } @@ -122,23 +123,23 @@ public static class ExpressionExecutionContextExtensions public static Variable CreateVariable(this ExpressionExecutionContext context, string name, T? value, Type? storageDriverType = null, Action? configure = default) { var existingVariable = context.GetVariable(name, localScopeOnly: true); - - if(existingVariable != null) + + if (existingVariable != null) throw new Exception($"Variable {name} already exists in the context."); - + var variable = new Variable(name, value) { StorageDriverType = storageDriverType ?? typeof(WorkflowStorageDriver) }; - + // Find the first parent context that has a variable container. // If not found, use the current context. var variableContainerContext = context.GetVariableContainerContext(); - + variableContainerContext.Set(variable, value, configure); return variable; } - + /// /// Returns the first parent context that contains a variable container. /// @@ -157,17 +158,17 @@ public static class ExpressionExecutionContextExtensions public static Variable SetVariable(this ExpressionExecutionContext context, string name, T? value, Action? configure = default) { var variable = context.GetVariable(name); - - if(variable == null) + + if (variable == null) return CreateVariable(context, name, value, configure: configure); // Get the context where the variable is defined. var contextWithVariable = context.FindContextContainingBlock(variable.Id) ?? context; - + // Set the value on the variable. variable.Value = value; variable.Set(contextWithVariable, value, configure); - + // Return the variable. return variable; } @@ -177,7 +178,20 @@ public static class ExpressionExecutionContextExtensions /// public static void Set(this ExpressionExecutionContext context, Output? output, object? value, Action? configure = default) { - if (output != null) context.Set(output.MemoryBlockReference(), value, configure); + if (output != null) + { + // Set the value on the output. + var outputMemoryBlockReference = output.MemoryBlockReference(); + context.Set(outputMemoryBlockReference, value, configure); + + // If the referenced output is a workflow output definition, set the value on the workflow execution context. + var workflowExecutionContext = context.GetWorkflowExecutionContext(); + var workflow = workflowExecutionContext.Workflow; + var workflowOutputDefinition = workflow.Outputs.FirstOrDefault(x => x.Name == outputMemoryBlockReference.Id); + + if (workflowOutputDefinition != null) + workflowExecutionContext.Output[workflowOutputDefinition.Name] = value!; + } } /// @@ -197,8 +211,8 @@ public static class ExpressionExecutionContextExtensions while (currentContext != null) { var register = currentContext.Memory; - - foreach (var entry in register.Blocks) + + foreach (var entry in register.Blocks) memoryBlocks.TryAdd(entry.Key, entry.Value); currentContext = currentContext.ParentContext; @@ -255,7 +269,7 @@ public static class ExpressionExecutionContextExtensions .Select(x => x.Name) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(); - + /// /// Gets all variables in scope. /// @@ -298,7 +312,7 @@ public static class ExpressionExecutionContextExtensions currentScope = currentScope.ParentContext; } } - + /// /// Returns the value of the specified input. /// @@ -309,7 +323,7 @@ public static class ExpressionExecutionContextExtensions public static T? GetInput(this ExpressionExecutionContext expressionExecutionContext, string name) { var value = expressionExecutionContext.GetInput(name); - return value != null ? (T) value : default; + return value != null ? (T)value : default; } /// @@ -322,18 +336,17 @@ public static class ExpressionExecutionContextExtensions { // If there's a variable in the current scope with the specified name, return that. var variable = expressionExecutionContext.GetVariable(name); - + if (variable != null) return variable.Get(expressionExecutionContext); - + // Otherwise, return the input. var workflowExecutionContext = expressionExecutionContext.GetWorkflowExecutionContext(); var input = workflowExecutionContext.Input; return input.TryGetValue(name, out var value) ? value : default; } - - + /// /// Returns the value of the specified input. /// @@ -375,10 +388,10 @@ public static class ExpressionExecutionContextExtensions { var activity = activityWithOutput.Activity; var activityDescriptor = activityWithOutput.ActivityDescriptor; - + var activityIdentifier = useActivityName ? activity.Name : activity.Id; var activityIdPascalName = activityIdentifier.Pascalize(); - + foreach (var output in activityDescriptor.Outputs) { var outputPascalName = output.Name.Pascalize(); @@ -442,7 +455,7 @@ public static class ExpressionExecutionContextExtensions } } } - + private static object ConvertIEnumerableToArray(object? obj) { if (obj == null) diff --git a/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivity.cs b/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivity.cs index db5126783..e3f9fdc08 100644 --- a/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivity.cs +++ b/src/modules/Elsa.Workflows.Management/Activities/WorkflowDefinitionActivity/WorkflowDefinitionActivity.cs @@ -50,7 +50,7 @@ public class WorkflowDefinitionActivity : Composite, IInitializable private async ValueTask OnChildCompletedAsync(ActivityCompletedContext context) { - var targetContext = context.TargetContext; + var activityExecutionContext = context.TargetContext; // Do we have a "complete composite" signal that triggered the completion? var completeCompositeSignal = context.WorkflowExecutionContext.TransientProperties.TryGetValue(nameof(CompleteCompositeSignal), out var signal) ? (CompleteCompositeSignal)signal : default; @@ -64,7 +64,7 @@ public class WorkflowDefinitionActivity : Composite, IInitializable } // Copy any collected outputs into the synthetic properties. - foreach (var outputDescriptor in targetContext.ActivityDescriptor.Outputs) + foreach (var outputDescriptor in activityExecutionContext.ActivityDescriptor.Outputs) { // Create a local scope variable for each output property. var variable = new Variable @@ -74,15 +74,15 @@ public class WorkflowDefinitionActivity : Composite, IInitializable }; // Use the variable to read the value from the memory. - var value = variable.Get(targetContext); + var value = variable.Get(activityExecutionContext); // Assign the value to the output synthetic property. var output = SyntheticProperties.TryGetValue(outputDescriptor.Name, out var outputValue) ? (Output?)outputValue : default; - targetContext.Set(output, value); + activityExecutionContext.Set(output, value); } // Complete this activity with the signal value. - await targetContext.CompleteActivityAsync(completeCompositeSignal?.Value); + await activityExecutionContext.CompleteActivityAsync(completeCompositeSignal?.Value); } private void CopyInputOutputToVariables(ActivityExecutionContext context) diff --git a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj index c2b5fedbd..2a9130e76 100644 --- a/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj +++ b/test/integration/Elsa.IntegrationTests/Elsa.IntegrationTests.csproj @@ -123,6 +123,9 @@ Always + + Always + diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowOutputMapping/Tests.cs b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowOutputMapping/Tests.cs new file mode 100644 index 000000000..04a4e46ca --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowOutputMapping/Tests.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Elsa.Testing.Shared; +using Xunit; +using Xunit.Abstractions; + +namespace Elsa.IntegrationTests.Scenarios.WorkflowOutputMapping; + +/// +/// Tests for mapping an activity's output directly to the workflow's output definition. +/// +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 = "Activity output mapped to workflow output definition is part of workflow instance output dictionary.")] + public async Task Test1() + { + // Populate registries. + await _services.PopulateRegistriesAsync(); + + // Import child workflow. + var workflowFileName = "Scenarios/WorkflowOutputMapping/Workflows/workflow-output.json"; + var workflowDefinition = await _services.ImportWorkflowDefinitionAsync(workflowFileName); + + // Execute. + var workflowState = await _services.RunWorkflowUntilEndAsync(workflowDefinition.DefinitionId); + + // Assert expected output. + var outputs = workflowState.Output; + Assert.Contains("Output1", outputs.Keys); + Assert.Equal("Foo", outputs["Output1"]); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowOutputMapping/Workflows/workflow-output.json b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowOutputMapping/Workflows/workflow-output.json new file mode 100644 index 000000000..30c6299f6 --- /dev/null +++ b/test/integration/Elsa.IntegrationTests/Scenarios/WorkflowOutputMapping/Workflows/workflow-output.json @@ -0,0 +1,85 @@ +{ + "id": "cf64d0eae6a213c3", + "definitionId": "f824b31493b04cb4", + "name": "Workflow Output", + "createdAt": "2023-11-08T20:33:42.959779+00:00", + "version": 1, + "toolVersion": "3.0.0.0", + "variables": [], + "inputs": [], + "outputs": [ + { + "type": "Object", + "name": "Output1", + "displayName": "Output 1", + "category": "Primitives", + "isArray": false + } + ], + "outcomes": [], + "customProperties": { + "Elsa:WorkflowContextProviderTypes": [] + }, + "isReadonly": false, + "isLatest": true, + "isPublished": true, + "options": { + "autoUpdateConsumingWorkflows": false + }, + "root": { + "type": "Elsa.Flowchart", + "version": 1, + "id": "caff0b58ffcbf993", + "nodeId": "Workflow1:caff0b58ffcbf993", + "metadata": {}, + "customProperties": { + "source": "FlowchartJsonConverter.cs:47", + "notFoundConnections": [], + "canStartWorkflow": false, + "runAsynchronously": false + }, + "activities": [ + { + "script": { + "typeName": "String", + "expression": { + "type": "Literal", + "value": "return \u0022Foo\u0022;" + }, + "memoryReference": { + "id": "966835c72852cc27:input-0" + } + }, + "possibleOutcomes": null, + "result": { + "typeName": "Object", + "memoryReference": { + "id": "Output1" + } + }, + "id": "966835c72852cc27", + "nodeId": "Workflow1:caff0b58ffcbf993:966835c72852cc27", + "name": "RunCSharp1", + "type": "Elsa.RunCSharp", + "version": 1, + "customProperties": { + "canStartWorkflow": false, + "runAsynchronously": false + }, + "metadata": { + "designer": { + "position": { + "x": -128.5, + "y": 302 + }, + "size": { + "width": 83.640625, + "height": 50 + } + } + } + } + ], + "connections": [] + } +} \ No newline at end of file