From 046b88f5d283f71e299925e6e89038456ad40ca1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 2 Mar 2025 11:17:18 +0100 Subject: [PATCH] Improved support for named workflow instances Introduced the ability to set and persist custom names for workflow instances. Updated relevant classes, services, and tests to ensure proper handling of the `Name` property. --- .../Elsa.Workflows.Core/Activities/SetName.cs | 14 ++---- .../Contexts/WorkflowExecutionContext.cs | 9 ++-- .../JsonWorkflowStateSerializer.cs | 2 +- .../Services/WorkflowRunner.cs | 22 ++++----- .../Services/WorkflowStateExtractor.cs | 2 + .../State/WorkflowState.cs | 11 +++-- .../Mappers/WorkflowStateMapper.cs | 16 ++----- .../Options/WorkflowInstanceOptions.cs | 5 ++ .../Services/WorkflowInstanceFactory.cs | 6 ++- ...eateAndRunWorkflowInstanceRequestMapper.cs | 6 ++- .../CreateWorkflowInstanceRequestMapper.cs | 6 ++- .../Proto/WorkflowInstance.Messages.proto | 18 ++++---- .../CreateAndRunWorkflowInstanceRequest.cs | 7 ++- .../Messages/CreateWorkflowInstanceRequest.cs | 7 ++- .../Services/LocalWorkflowClient.cs | 3 ++ .../SetNameTests.cs | 4 +- .../DefaultRuntimeTests.cs | 2 +- .../WorkflowInstanceNameTests.cs | 46 +++++++++++++++++++ .../Workflows/NamedWorkflow.cs | 12 +++++ 19 files changed, 141 insertions(+), 57 deletions(-) create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/WorkflowInstanceNameTests.cs create mode 100644 test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/Workflows/NamedWorkflow.cs diff --git a/src/modules/Elsa.Workflows.Core/Activities/SetName.cs b/src/modules/Elsa.Workflows.Core/Activities/SetName.cs index 3c4fbf1df..82e565c49 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/SetName.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/SetName.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Elsa.Extensions; using Elsa.Workflows.Attributes; using Elsa.Workflows.Models; using JetBrains.Annotations; @@ -12,18 +13,13 @@ namespace Elsa.Workflows.Activities; [PublicAPI] public class SetName : CodeActivity { - /// - /// The property key name used to store the workflow instance name. - /// - public const string WorkflowInstanceNameKey = "WorkflowInstanceName"; - /// - public SetName([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + public SetName([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line) { } /// - public SetName(Input value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) + public SetName(Input value, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line) { Value = value; } @@ -36,7 +32,7 @@ public class SetName : CodeActivity /// protected override void Execute(ActivityExecutionContext context) { - var value = context.Get(Value); - context.WorkflowExecutionContext.SetProperty(WorkflowInstanceNameKey, value!); + var value = Value.GetOrDefault(context); + context.WorkflowExecutionContext.Name = value; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 345295d32..f57350d48 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -71,8 +71,8 @@ public partial class WorkflowExecutionContext : IExecutionContext _activityExecutionContexts = new List(); Scheduler = serviceProvider.GetRequiredService().CreateScheduler(); IdentityGenerator = serviceProvider.GetRequiredService(); - Input = input != null ? new Dictionary(input, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); - Properties = properties != null ? new Dictionary(properties, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); + Input = input != null ? new(input, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); + Properties = properties != null ? new(properties, StringComparer.OrdinalIgnoreCase) : new Dictionary(StringComparer.OrdinalIgnoreCase); ExecuteDelegate = executeDelegate; TriggerActivityId = triggerActivityId; CreatedAt = createdAt; @@ -193,7 +193,7 @@ public partial class WorkflowExecutionContext : IExecutionContext MemoryRegister = workflowGraph.Workflow.CreateRegister() }; - workflowExecutionContext.ExpressionExecutionContext = new ExpressionExecutionContext(serviceProvider, workflowExecutionContext.MemoryRegister, cancellationToken: cancellationToken); + workflowExecutionContext.ExpressionExecutionContext = new(serviceProvider, workflowExecutionContext.MemoryRegister, cancellationToken: cancellationToken); await workflowExecutionContext.SetWorkflowGraphAsync(workflowGraph); return workflowExecutionContext; @@ -253,6 +253,9 @@ public partial class WorkflowExecutionContext : IExecutionContext /// An application-specific identifier associated with the execution context. public string? CorrelationId { get; set; } + /// Gets or sets the name of the workflow instance. + public string? Name { get; set; } + /// The ID of the workflow instance that triggered this instance. public string? ParentWorkflowInstanceId { get; set; } diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs index d23869f4a..ad664f7e8 100644 --- a/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Serialization/Serializers/JsonWorkflowStateSerializer.cs @@ -128,7 +128,7 @@ public class JsonWorkflowStateSerializer : ConfigurableSerializer, IWorkflowStat public override JsonSerializerOptions GetOptions() { var options = base.GetOptions(); - return new JsonSerializerOptions(options) + return new(options) { ReferenceHandler = new CrossScopedReferenceHandler() }; diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs index a79842799..46ae8efdb 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs @@ -25,7 +25,7 @@ public class WorkflowRunner( : IWorkflowRunner { /// - public async Task RunAsync(IActivity activity, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task RunAsync(IActivity activity, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var workflow = Workflow.FromActivity(activity); var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow, cancellationToken); @@ -33,7 +33,7 @@ public class WorkflowRunner( } /// - public async Task RunAsync(IWorkflow workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task RunAsync(IWorkflow workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var builder = workflowBuilderFactory.CreateBuilder(); var workflowDefinition = await builder.BuildWorkflowAsync(workflow, cancellationToken); @@ -41,14 +41,14 @@ public class WorkflowRunner( } /// - public async Task> RunAsync(WorkflowBase workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task> RunAsync(WorkflowBase workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var result = await RunAsync((IWorkflow)workflow, options, cancellationToken); return new(result.WorkflowState, result.Workflow, (TResult)result.Result!); } /// - public async Task RunAsync(RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) where T : IWorkflow, new() + public async Task RunAsync(RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) where T : IWorkflow, new() { var builder = workflowBuilderFactory.CreateBuilder(); var workflowDefinition = await builder.BuildWorkflowAsync(cancellationToken); @@ -56,7 +56,7 @@ public class WorkflowRunner( } /// - public async Task RunAsync(RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) where T : WorkflowBase, new() + public async Task RunAsync(RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) where T : WorkflowBase, new() { var builder = workflowBuilderFactory.CreateBuilder(); var workflow = await builder.BuildWorkflowAsync(cancellationToken); @@ -65,7 +65,7 @@ public class WorkflowRunner( } /// - public async Task RunAsync(WorkflowGraph workflowGraph, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task RunAsync(WorkflowGraph workflowGraph, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { // Set up a workflow execution context. var instanceId = options?.WorkflowInstanceId ?? identityGenerator.GenerateId(); @@ -82,7 +82,7 @@ public class WorkflowRunner( parentWorkflowInstanceId, input, properties, - default, + null, triggerActivityId, cancellationToken); @@ -93,21 +93,21 @@ public class WorkflowRunner( } /// - public async Task RunAsync(Workflow workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task RunAsync(Workflow workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow, cancellationToken); return await RunAsync(workflowGraph, options, cancellationToken); } /// - public async Task RunAsync(Workflow workflow, WorkflowState workflowState, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task RunAsync(Workflow workflow, WorkflowState workflowState, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { var workflowGraph = await workflowGraphBuilder.BuildAsync(workflow, cancellationToken); return await RunAsync(workflowGraph, workflowState, options, cancellationToken); } /// - public async Task RunAsync(WorkflowGraph workflowGraph, WorkflowState workflowState, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) + public async Task RunAsync(WorkflowGraph workflowGraph, WorkflowState workflowState, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) { // Create a workflow execution context. var input = options?.Input; @@ -123,7 +123,7 @@ public class WorkflowRunner( parentWorkflowInstanceId, input, properties, - default, + null, triggerActivityId, cancellationToken); diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs index 99a11a214..e63e1c92c 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowStateExtractor.cs @@ -18,6 +18,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor DefinitionVersionId = workflowExecutionContext.Workflow.Identity.Id, DefinitionVersion = workflowExecutionContext.Workflow.Identity.Version, CorrelationId = workflowExecutionContext.CorrelationId, + Name = workflowExecutionContext.Name, ParentWorkflowInstanceId = workflowExecutionContext.ParentWorkflowInstanceId, Status = workflowExecutionContext.Status, SubStatus = workflowExecutionContext.SubStatus, @@ -45,6 +46,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor { workflowExecutionContext.Id = state.Id; workflowExecutionContext.CorrelationId = state.CorrelationId; + workflowExecutionContext.Name = state.Name; workflowExecutionContext.ParentWorkflowInstanceId = state.ParentWorkflowInstanceId; workflowExecutionContext.SubStatus = state.SubStatus; workflowExecutionContext.Bookmarks = state.Bookmarks; diff --git a/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs b/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs index 8bc2cd1a0..43887b16b 100644 --- a/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs +++ b/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs @@ -11,17 +11,17 @@ public class WorkflowState /// /// Gets or sets the ID. /// - public string Id { get; set; } = default!; + public string Id { get; set; } = null!; /// /// The workflow definition ID. /// - public string DefinitionId { get; set; } = default!; + public string DefinitionId { get; set; } = null!; /// /// The workflow definition version ID. /// - public string DefinitionVersionId { get; set; } = default!; + public string DefinitionVersionId { get; set; } = null!; /// /// The workflow definition version. @@ -37,6 +37,11 @@ public class WorkflowState /// The correlation ID of the workflow, if any. /// public string? CorrelationId { get; set; } + + /// + /// Gets or sets the name of the workflow instance. + /// + public string? Name { get; set; } /// /// The status of the workflow. diff --git a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowStateMapper.cs b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowStateMapper.cs index 30af5ac0c..c909073db 100644 --- a/src/modules/Elsa.Workflows.Management/Mappers/WorkflowStateMapper.cs +++ b/src/modules/Elsa.Workflows.Management/Mappers/WorkflowStateMapper.cs @@ -1,5 +1,3 @@ -using Elsa.Extensions; -using Elsa.Workflows.Activities; using Elsa.Workflows.Management.Entities; using Elsa.Workflows.State; @@ -16,14 +14,14 @@ public class WorkflowStateMapper public WorkflowInstance? Map(WorkflowState? source) { if (source == null) - return default; + return null; var workflowInstance = new WorkflowInstance(); Apply(source, workflowInstance); return workflowInstance; } - + /// /// Maps a workflow state to a workflow instance. /// @@ -38,14 +36,12 @@ public class WorkflowStateMapper target.Status = source.Status; target.SubStatus = source.SubStatus; target.CorrelationId = source.CorrelationId; + target.Name = source.Name; target.IncidentCount = source.Incidents.Count; target.IsSystem = source.IsSystem; target.UpdatedAt = source.UpdatedAt; target.FinishedAt = source.FinishedAt; target.WorkflowState = source; - - if (source.Properties.TryGetValue(SetName.WorkflowInstanceNameKey, out var name)) - target.Name = name; } /// @@ -54,7 +50,7 @@ public class WorkflowStateMapper public WorkflowState? Map(WorkflowInstance? source) { if (source == null) - return default; + return null; var workflowState = source.WorkflowState; workflowState.Id = source.Id; @@ -66,13 +62,11 @@ public class WorkflowStateMapper workflowState.Status = source.Status; workflowState.SubStatus = source.SubStatus; workflowState.CorrelationId = source.CorrelationId; + workflowState.Name = source.Name; workflowState.UpdatedAt = source.UpdatedAt; workflowState.FinishedAt = source.FinishedAt; workflowState.IsSystem = source.IsSystem; - if (source.Name != null) - workflowState.Properties[SetName.WorkflowInstanceNameKey] = source.Name; - return workflowState; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Options/WorkflowInstanceOptions.cs b/src/modules/Elsa.Workflows.Management/Options/WorkflowInstanceOptions.cs index 9cdc133ad..76051aed2 100644 --- a/src/modules/Elsa.Workflows.Management/Options/WorkflowInstanceOptions.cs +++ b/src/modules/Elsa.Workflows.Management/Options/WorkflowInstanceOptions.cs @@ -13,6 +13,11 @@ public class WorkflowInstanceOptions /// public string? CorrelationId { get; set; } + /// + /// The name of the workflow instance. + /// + public string? Name { get; set; } + /// /// The input to the workflow instance, if any. /// diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceFactory.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceFactory.cs index 9175158d4..aada085a6 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceFactory.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowInstanceFactory.cs @@ -14,13 +14,14 @@ public class WorkflowInstanceFactory(IIdentityGenerator identityGenerator, ISyst public WorkflowState CreateWorkflowState(Workflow workflow, WorkflowInstanceOptions? options = null) { var now = systemClock.UtcNow; - return new WorkflowState + return new() { Id = string.IsNullOrEmpty(options?.WorkflowInstanceId) ? identityGenerator.GenerateId() : options.WorkflowInstanceId, DefinitionId = workflow.Identity.DefinitionId, DefinitionVersionId = workflow.Identity.Id, DefinitionVersion = workflow.Identity.Version, CorrelationId = options?.CorrelationId, + Name = options?.Name, Input = options?.Input ?? new Dictionary(), Properties = options?.Properties ?? new Dictionary(), Status = WorkflowStatus.Running, @@ -36,7 +37,7 @@ public class WorkflowInstanceFactory(IIdentityGenerator identityGenerator, ISyst public WorkflowInstance CreateWorkflowInstance(Workflow workflow, WorkflowInstanceOptions? options = null) { var workflowState = CreateWorkflowState(workflow, options); - return new WorkflowInstance + return new() { Id = workflowState.Id, ParentWorkflowInstanceId = workflowState.ParentWorkflowInstanceId, @@ -45,6 +46,7 @@ public class WorkflowInstanceFactory(IIdentityGenerator identityGenerator, ISyst DefinitionVersionId = workflowState.DefinitionVersionId, Version = workflowState.DefinitionVersion, CorrelationId = workflowState.CorrelationId, + Name = workflowState.Name, Status = workflowState.Status, SubStatus = workflowState.SubStatus, IncidentCount = workflowState.Incidents.Count, diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateAndRunWorkflowInstanceRequestMapper.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateAndRunWorkflowInstanceRequestMapper.cs index 14caababa..b81c07960 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateAndRunWorkflowInstanceRequestMapper.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateAndRunWorkflowInstanceRequestMapper.cs @@ -23,6 +23,7 @@ public class CreateAndRunWorkflowInstanceRequestMapper(WorkflowDefinitionHandleM WorkflowDefinitionHandle = workflowDefinitionHandleMapper.Map(source.WorkflowDefinitionHandle), WorkflowInstanceId = workflowInstanceId.EmptyIfNull(), CorrelationId = source.CorrelationId.EmptyIfNull(), + Name = source.Name.EmptyIfNull(), ParentId = source.ParentId.EmptyIfNull(), Input = source.Input?.SerializeInput() ?? new ProtoInput(), Properties = source.Properties?.SerializeProperties() ?? new ProtoProperties(), @@ -41,8 +42,9 @@ public class CreateAndRunWorkflowInstanceRequestMapper(WorkflowDefinitionHandleM return new() { WorkflowDefinitionHandle = workflowDefinitionHandleMapper.Map(source.WorkflowDefinitionHandle), - CorrelationId = source.CorrelationId, - ParentId = source.ParentId, + CorrelationId = source.CorrelationId.NullIfEmpty(), + Name = source.Name.NullIfEmpty(), + ParentId = source.ParentId.NullIfEmpty(), Input = source.Input?.DeserializeInput(), Properties = source.Properties?.DeserializeProperties(), ActivityHandle = activityHandleMapper.Map(source.ActivityHandle), diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateWorkflowInstanceRequestMapper.cs b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateWorkflowInstanceRequestMapper.cs index 2c0358ebe..0d8ce651a 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateWorkflowInstanceRequestMapper.cs +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Mappers/CreateWorkflowInstanceRequestMapper.cs @@ -24,6 +24,7 @@ public class CreateWorkflowInstanceRequestMapper(WorkflowDefinitionHandleMapper WorkflowDefinitionHandle = workflowDefinitionHandleMapper.Map(source.WorkflowDefinitionHandle), WorkflowInstanceId = workflowInstanceId, CorrelationId = source.CorrelationId.EmptyIfNull(), + Name = source.Name.EmptyIfNull(), ParentId = source.ParentId.EmptyIfNull(), Input = source.Input?.SerializeInput() ?? new ProtoInput(), Properties = source.Properties?.SerializeProperties() ?? new ProtoProperties() @@ -40,8 +41,9 @@ public class CreateWorkflowInstanceRequestMapper(WorkflowDefinitionHandleMapper return new() { WorkflowDefinitionHandle = workflowDefinitionHandleMapper.Map(source.WorkflowDefinitionHandle), - CorrelationId = source.CorrelationId, - ParentId = source.ParentId, + CorrelationId = source.CorrelationId.NullIfEmpty(), + Name = source.Name.NullIfEmpty(), + ParentId = source.ParentId.NullIfEmpty(), Input = source.Input?.DeserializeInput(), Properties = source.Properties?.DeserializeProperties() }; diff --git a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto index 719572c28..bb1683fab 100644 --- a/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto +++ b/src/modules/Elsa.Workflows.Runtime.ProtoActor/Proto/WorkflowInstance.Messages.proto @@ -45,9 +45,10 @@ message CreateWorkflowInstanceRequest{ WorkflowDefinitionHandle WorkflowDefinitionHandle = 1; string WorkflowInstanceId = 2; optional string CorrelationId = 3; - optional string ParentId = 4; - optional Input input = 5; - optional Properties properties = 6; + optional string Name = 4; + optional string ParentId = 5; + optional Input input = 6; + optional Properties properties = 7; } message CreateWorkflowInstanceResponse{ @@ -71,11 +72,12 @@ message CreateAndRunWorkflowInstanceRequest{ WorkflowDefinitionHandle WorkflowDefinitionHandle = 1; string WorkflowInstanceId = 2; optional string CorrelationId = 3; - optional string ParentId = 4; - optional Input input = 5; - optional Properties properties = 6; - optional ActivityHandle ActivityHandle = 7; - optional string TriggerActivityId = 8; + optional string Name = 4; + optional string ParentId = 5; + optional Input input = 6; + optional Properties properties = 7; + optional ActivityHandle ActivityHandle = 8; + optional string TriggerActivityId = 9; } message ExportWorkflowStateResponse { diff --git a/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs b/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs index 07c812077..69901ec03 100644 --- a/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs @@ -12,12 +12,17 @@ public class CreateAndRunWorkflowInstanceRequest /// /// The ID of the workflow definition version to create an instance of. /// - public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = default!; + public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!; /// /// The correlation ID of the workflow, if any. /// public string? CorrelationId { get; set; } + + /// + /// The name of the workflow instance to be created. + /// + public string? Name { get; set; } /// /// The input to the workflow instance, if any. diff --git a/src/modules/Elsa.Workflows.Runtime/Messages/CreateWorkflowInstanceRequest.cs b/src/modules/Elsa.Workflows.Runtime/Messages/CreateWorkflowInstanceRequest.cs index ab55c3891..8cebf9d6f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Messages/CreateWorkflowInstanceRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Messages/CreateWorkflowInstanceRequest.cs @@ -12,13 +12,18 @@ public class CreateWorkflowInstanceRequest /// /// The ID of the workflow definition version to create an instance of. /// - public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = default!; + public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!; /// /// The correlation ID of the workflow, if any. /// public string? CorrelationId { get; set; } + /// + /// The name of the workflow instance to be created. + /// + public string? Name { get; set; } + /// /// The input to the workflow instance, if any. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index 8497c99dc..358849bb7 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -35,6 +35,7 @@ public class LocalWorkflowClient( { WorkflowInstanceId = WorkflowInstanceId, CorrelationId = request.CorrelationId, + Name = request.Name, ParentWorkflowInstanceId = request.ParentId, Input = request.Input, Properties = request.Properties @@ -58,6 +59,7 @@ public class LocalWorkflowClient( { Properties = request.Properties, CorrelationId = request.CorrelationId, + Name = request.Name, Input = request.Input, WorkflowDefinitionHandle = request.WorkflowDefinitionHandle, ParentId = request.ParentId @@ -148,6 +150,7 @@ public class LocalWorkflowClient( { WorkflowInstanceId = WorkflowInstanceId, CorrelationId = request.CorrelationId, + Name = request.Name, ParentWorkflowInstanceId = request.ParentId, Input = request.Input, Properties = request.Properties diff --git a/test/integration/Elsa.Activities.IntegrationTests/SetNameTests.cs b/test/integration/Elsa.Activities.IntegrationTests/SetNameTests.cs index 3524b588d..91de62ddd 100644 --- a/test/integration/Elsa.Activities.IntegrationTests/SetNameTests.cs +++ b/test/integration/Elsa.Activities.IntegrationTests/SetNameTests.cs @@ -15,13 +15,13 @@ public class SetNameTests _serviceProvider = new TestApplicationBuilder(testOutputHelper).WithCapturingTextWriter(_capturingTextWriter).Build(); } - [Fact(DisplayName = "WriteLine prints the expected line to the console.")] + [Fact(DisplayName = "SetName sets the workflow instance name.")] public async Task Test1() { const string expectedName = "Foo"; var setName = new SetName(new Input(expectedName)); var result = await _serviceProvider.RunActivityAsync(setName); - var actualName = result.WorkflowState.Properties[SetName.WorkflowInstanceNameKey]; + var actualName = result.WorkflowState.Name; Assert.Equal(expectedName, actualName); } } \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs index 2cad71e66..704efe89c 100644 --- a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowCancellation/DefaultRuntimeTests.cs @@ -59,7 +59,7 @@ public class DefaultRuntimeTests const string workflowDefinitionId = nameof(SimpleSuspendedWorkflow); var workflowClient = await _workflowRuntime.CreateClientAsync(); - await workflowClient.CreateInstanceAsync(new CreateWorkflowInstanceRequest + await workflowClient.CreateInstanceAsync(new() { WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(workflowDefinitionId, VersionOptions.Published) }); diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/WorkflowInstanceNameTests.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/WorkflowInstanceNameTests.cs new file mode 100644 index 000000000..b7ebfb427 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/WorkflowInstanceNameTests.cs @@ -0,0 +1,46 @@ +using Elsa.Common.Models; +using Elsa.Testing.Shared; +using Elsa.Workflows.IntegrationTests.Scenarios.WorkflowInstanceName.Workflows; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.Runtime.Messages; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowInstanceName; + +public class WorkflowInstanceNameTests +{ + private readonly IServiceProvider _services; + private readonly CapturingTextWriter _capturingTextWriter = new(); + private readonly IWorkflowRuntime _workflowRuntime; + + public WorkflowInstanceNameTests(ITestOutputHelper testOutputHelper) + { + _services = new TestApplicationBuilder(testOutputHelper) + .WithCapturingTextWriter(_capturingTextWriter) + .AddWorkflow() + .Build(); + + _workflowRuntime = _services.GetRequiredService(); + } + + [Fact(DisplayName = "Setting a workflow instance name keeps the workflow instance name when the workflow is executed")] + public async Task SuspendedCancelTest() + { + await _services.PopulateRegistriesAsync(); + const string workflowDefinitionId = nameof(NamedWorkflow); + var desiredName = Guid.NewGuid().ToString(); + var workflowClient = await _workflowRuntime.CreateClientAsync(); + await workflowClient.CreateInstanceAsync(new() + { + Name = desiredName, + WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionId(workflowDefinitionId, VersionOptions.Published) + }); + await workflowClient.RunInstanceAsync(RunWorkflowInstanceRequest.Empty); + var workflowState = await workflowClient.ExportStateAsync(); + + Assert.Equal([desiredName], _capturingTextWriter.Lines); + Assert.Equal(desiredName, workflowState.Name); + } +} \ No newline at end of file diff --git a/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/Workflows/NamedWorkflow.cs b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/Workflows/NamedWorkflow.cs new file mode 100644 index 000000000..62a2aaff8 --- /dev/null +++ b/test/integration/Elsa.Workflows.IntegrationTests/Scenarios/WorkflowInstanceName/Workflows/NamedWorkflow.cs @@ -0,0 +1,12 @@ +using Elsa.Extensions; +using Elsa.Workflows.Activities; + +namespace Elsa.Workflows.IntegrationTests.Scenarios.WorkflowInstanceName.Workflows; + +public class NamedWorkflow : WorkflowBase +{ + protected override void Build(IWorkflowBuilder builder) + { + builder.Root = new WriteLine(x => x.GetWorkflowExecutionContext().Name); + } +} \ No newline at end of file