Add support for direct-output binding
This change enables activity output to be directly bound to workflow output.
This commit is contained in:
parent
f3beca94dc
commit
cd33d29768
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\bundles\Elsa\Elsa.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.CSharp\Elsa.CSharp.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.JavaScript\Elsa.JavaScript.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Liquid\Elsa.Liquid.csproj" />
|
||||
<ProjectReference Include="..\..\modules\Elsa.Scheduling\Elsa.Scheduling.csproj" />
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public class TestApplicationBuilder
|
|||
_configureElsa += elsa => elsa
|
||||
.AddActivitiesFrom<WriteLine>()
|
||||
.UseScheduling()
|
||||
.UseCSharp()
|
||||
.UseJavaScript()
|
||||
.UseLiquid()
|
||||
.UseDsl()
|
||||
|
|
|
|||
|
|
@ -21,16 +21,17 @@ public static class ExpressionExecutionContextExtensions
|
|||
/// The key used to store the <see cref="WorkflowExecutionContext"/> in the <see cref="ExpressionExecutionContext.TransientProperties"/> dictionary.
|
||||
/// </summary>
|
||||
public static readonly object WorkflowExecutionContextKey = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The key used to store the <see cref="ActivityExecutionContext"/> in the <see cref="ExpressionExecutionContext.TransientProperties"/> dictionary.
|
||||
/// </summary>
|
||||
public static readonly object ActivityExecutionContextKey = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The
|
||||
/// </summary>
|
||||
public static readonly object InputKey = new();
|
||||
|
||||
public static readonly object WorkflowKey = new();
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -64,14 +65,14 @@ public static class ExpressionExecutionContextExtensions
|
|||
/// Returns the <see cref="WorkflowExecutionContext"/> of the specified <see cref="ExpressionExecutionContext"/>
|
||||
/// </summary>
|
||||
public static WorkflowExecutionContext GetWorkflowExecutionContext(this ExpressionExecutionContext context) => (WorkflowExecutionContext)context.TransientProperties[WorkflowExecutionContextKey];
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="ActivityExecutionContext"/> of the specified <see cref="ExpressionExecutionContext"/>
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public static ActivityExecutionContext GetActivityExecutionContext(this ExpressionExecutionContext context) => (ActivityExecutionContext)context.TransientProperties[ActivityExecutionContextKey];
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="ActivityExecutionContext"/> of the specified <see cref="ExpressionExecutionContext"/>
|
||||
/// </summary>
|
||||
|
|
@ -84,18 +85,18 @@ public static class ExpressionExecutionContextExtensions
|
|||
/// Returns the value of the specified input.
|
||||
/// </summary>
|
||||
public static T? Get<T>(this ExpressionExecutionContext context, Input<T>? input) => input != null ? context.GetBlock(input.MemoryBlockReference).Value.ConvertTo<T>() : default;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the specified output.
|
||||
/// </summary>
|
||||
public static T? Get<T>(this ExpressionExecutionContext context, Output output) => context.GetBlock(output.MemoryBlockReference).Value.ConvertTo<T>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the specified output.
|
||||
/// </summary>
|
||||
public static object? Get(this ExpressionExecutionContext context, Output output) => context.GetBlock(output.MemoryBlockReference).Value;
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the variable with the specified name.
|
||||
/// </summary>
|
||||
|
|
@ -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<T>(this ExpressionExecutionContext context, string name, T? value, Type? storageDriverType = null, Action<MemoryBlock>? 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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first parent context that contains a variable container.
|
||||
/// </summary>
|
||||
|
|
@ -157,17 +158,17 @@ public static class ExpressionExecutionContextExtensions
|
|||
public static Variable SetVariable<T>(this ExpressionExecutionContext context, string name, T? value, Action<MemoryBlock>? 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
|
|||
/// </summary>
|
||||
public static void Set(this ExpressionExecutionContext context, Output? output, object? value, Action<MemoryBlock>? 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!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets all variables in scope.
|
||||
/// </summary>
|
||||
|
|
@ -298,7 +312,7 @@ public static class ExpressionExecutionContextExtensions
|
|||
currentScope = currentScope.ParentContext;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the specified input.
|
||||
/// </summary>
|
||||
|
|
@ -309,7 +323,7 @@ public static class ExpressionExecutionContextExtensions
|
|||
public static T? GetInput<T>(this ExpressionExecutionContext expressionExecutionContext, string name)
|
||||
{
|
||||
var value = expressionExecutionContext.GetInput(name);
|
||||
return value != null ? (T) value : default;
|
||||
return value != null ? (T)value : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of the specified input.
|
||||
/// </summary>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -123,6 +123,9 @@
|
|||
<None Update="Scenarios\ParentChildOutputMapping\Workflows\sum.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Scenarios\WorkflowOutputMapping\Workflows\workflow-output.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for mapping an activity's output directly to the workflow's output definition.
|
||||
/// </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 = "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"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue