Add functionality to execute nested workflows (#6137)

Introduced a new `ExecuteWorkflow` activity that allows executing nested workflows. Added `ExecuteWorkflowResult` model to handle results and integrated component tests to verify nested workflow execution. Enhanced `IWorkflowBuilder` to support fluent methods for adding outputs.
This commit is contained in:
Sipke Schoorstra 2024-11-21 19:18:47 +01:00 committed by GitHub
parent 492b16bd24
commit a6bd386005
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 258 additions and 8 deletions

View file

@ -114,14 +114,7 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer
/// <inheritdoc />
public InputDefinition WithInput<T>(string name, string? description = default)
{
return WithInput(inputDefinition =>
{
inputDefinition.Name = name;
inputDefinition.Type = typeof(T);
if (description != null)
inputDefinition.Description = description;
});
return WithInput(name, typeof(T), description);
}
/// <inheritdoc />
@ -164,6 +157,46 @@ public class WorkflowBuilder(IActivityVisitor activityVisitor, IIdentityGraphSer
return this;
}
public OutputDefinition WithOutput<T>(string name, string? description = default)
{
return WithOutput(name, typeof(T), description);
}
public OutputDefinition WithOutput(string name, Type type, string? description = default)
{
return WithOutput(outputDefinition =>
{
outputDefinition.Name = name;
outputDefinition.Type = type;
if (description != null)
outputDefinition.Description = description;
});
}
public OutputDefinition WithOutput(string name, Type type, Action<OutputDefinition>? setup = default)
{
return WithOutput(outputDefinition =>
{
outputDefinition.Name = name;
outputDefinition.Type = type;
setup?.Invoke(outputDefinition);
});
}
public OutputDefinition WithOutput(Action<OutputDefinition> setup)
{
var outputDefinition = new OutputDefinition();
setup(outputDefinition);
return WithOutput(outputDefinition);
}
public OutputDefinition WithOutput(OutputDefinition outputDefinition)
{
Outputs.Add(outputDefinition);
return outputDefinition;
}
/// <inheritdoc />
public IWorkflowBuilder WithCustomProperty(string name, object value)
{

View file

@ -145,6 +145,31 @@ public interface IWorkflowBuilder
/// A fluent method for adding an input to <see cref="Inputs"/>.
/// </summary>
IWorkflowBuilder WithInput(InputDefinition inputDefinition);
/// <summary>
/// A fluent method for adding an output to <see cref="Outputs"/>.
/// </summary>
OutputDefinition WithOutput<T>(string name, string? description = default);
/// <summary>
/// A fluent method for adding an output to <see cref="Outputs"/>.
/// </summary>
OutputDefinition WithOutput(string name, Type type, string? description = default);
/// <summary>
/// A fluent method for adding an output to <see cref="Outputs"/>.
/// </summary>
OutputDefinition WithOutput(string name, Type type, Action<OutputDefinition>? setup = default);
/// <summary>
/// A fluent method for adding an output to <see cref="Outputs"/>.
/// </summary>
OutputDefinition WithOutput(Action<OutputDefinition> setup);
/// <summary>
/// A fluent method for adding an output to <see cref="Outputs"/>.
/// </summary>
OutputDefinition WithOutput(OutputDefinition outputDefinition);
/// <summary>
/// A fluent method for adding a property to <see cref="CustomProperties"/>.

View file

@ -0,0 +1,91 @@
using System.Runtime.CompilerServices;
using Elsa.Common.Models;
using Elsa.Extensions;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management;
using Elsa.Workflows.Models;
using Elsa.Workflows.Options;
using Elsa.Workflows.UIHints;
using JetBrains.Annotations;
namespace Elsa.Workflows.Runtime.Activities;
/// <summary>
/// Creates a new workflow instance of the specified workflow and dispatches it for execution.
/// </summary>
[Activity("Elsa", "Composition", "Create a new workflow instance of the specified workflow and execute it.", Kind = ActivityKind.Task)]
[UsedImplicitly]
public class ExecuteWorkflow : Activity<ExecuteWorkflowResult>
{
/// <inheritdoc />
public ExecuteWorkflow([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The definition ID of the workflow to execute.
/// </summary>
[Input(
DisplayName = "Workflow Definition",
Description = "The definition ID of the workflow to execute.",
UIHint = InputUIHints.WorkflowDefinitionPicker
)]
public Input<string> WorkflowDefinitionId { get; set; } = default!;
/// <summary>
/// The correlation ID to associate the workflow with.
/// </summary>
[Input(
DisplayName = "Correlation ID",
Description = "The correlation ID to associate the workflow with."
)]
public Input<string?> CorrelationId { get; set; } = default!;
/// <summary>
/// The input to send to the workflow.
/// </summary>
[Input(Description = "The input to send to the workflow.")]
public Input<IDictionary<string, object>?> Input { get; set; } = default!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var result = await ExecuteWorkflowAsync(context);
context.SetResult(result);
await context.CompleteActivityAsync();
}
private async ValueTask<ExecuteWorkflowResult> ExecuteWorkflowAsync(ActivityExecutionContext context)
{
var workflowDefinitionId = WorkflowDefinitionId.Get(context);
var input = Input.GetOrDefault(context) ?? new Dictionary<string, object>();
var correlationId = CorrelationId.GetOrDefault(context);
var workflowInvoker = context.GetRequiredService<IWorkflowRunner>();
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
var workflowDefinitionService = context.GetRequiredService<IWorkflowDefinitionService>();
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken);
if (workflowGraph == null)
throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found.");
var options = new RunWorkflowOptions
{
ParentWorkflowInstanceId = context.WorkflowExecutionContext.Id,
Input = input,
CorrelationId = correlationId,
WorkflowInstanceId = identityGenerator.GenerateId()
};
var workflowResult = await workflowInvoker.RunAsync(workflowGraph, options, context.CancellationToken);
var info = new ExecuteWorkflowResult
{
WorkflowInstanceId = options.WorkflowInstanceId,
Status = workflowResult.WorkflowState.Status,
SubStatus = workflowResult.WorkflowState.SubStatus,
Output = workflowResult.WorkflowState.Output
};
return info;
}
}

View file

@ -0,0 +1,14 @@
namespace Elsa.Workflows.Runtime;
/// <summary>
/// Represents the result of executing a workflow.
/// </summary>
public class ExecuteWorkflowResult
{
public string WorkflowDefinitionVersionId { get; set; } = default!;
public string WorkflowInstanceId { get; set; } = default!;
public string? CorrelationId { get; set; }
public WorkflowStatus Status { get; set; }
public WorkflowSubStatus SubStatus { get; set; }
public IDictionary<string, object>? Output { get; set; }
}

View file

@ -0,0 +1,21 @@
using Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows.Workflows;
using Elsa.Workflows.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows;
public class ExecuteWorkflowsTests : AppComponentTest
{
private readonly IWorkflowRunner _workflowRunner;
public ExecuteWorkflowsTests(App app) : base(app)
{
_workflowRunner = Scope.ServiceProvider.GetRequiredService<IWorkflowRunner>();
}
[Fact]
public async Task ExecuteWorkflow_ShouldExecuteWorkflow()
{
await _workflowRunner.RunAsync<MainWorkflow>();
}
}

View file

@ -0,0 +1,34 @@
using System.Text.Json;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Runtime;
using Elsa.Workflows.Runtime.Activities;
namespace Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows.Workflows;
public class MainWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var workflowResult = builder.WithVariable<ExecuteWorkflowResult>();
builder.Root = new Sequence
{
Activities =
{
new ExecuteWorkflow
{
WorkflowDefinitionId = new(SubroutineWorkflow.DefinitionId),
Input = new(new Dictionary<string, object>
{
["Value"] = 21
}),
Result = new(workflowResult)
},
new WriteLine(context => $"Subroutine output: {JsonSerializer.Serialize(workflowResult.Get(context))}")
}
};
}
}

View file

@ -0,0 +1,32 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Activities.SetOutput;
using Hangfire.Annotations;
namespace Elsa.Workflows.ComponentTests.Scenarios.ExecuteWorkflows.Workflows;
[UsedImplicitly]
public class SubroutineWorkflow : WorkflowBase
{
public static readonly string DefinitionId = Guid.NewGuid().ToString();
protected override void Build(IWorkflowBuilder builder)
{
builder.WithDefinitionId(DefinitionId);
var valueInput = builder.WithInput<double>("Value");
var output = builder.WithOutput<double>("Output");
builder.Root = new Sequence
{
Activities =
{
new WriteLine(context => $"Running subroutine on value {context.GetInput<double>(valueInput)}..."),
new SetOutput
{
OutputName = new(output.Name),
OutputValue = new(context => context.GetInput<double>(valueInput) * 2)
}
}
};
}
}