From b2d3fd9fda9bb150899146ff5ddca3c6f253974d Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 26 Feb 2025 14:53:18 +0100 Subject: [PATCH 1/5] Add configuration for default incident handling strategy Configured `IncidentOptions` to set `DefaultIncidentStrategy` to `ContinueWithIncidentsStrategy`. This ensures consistent handling of workflow incidents and improves customization flexibility for incident management. --- src/apps/Elsa.Server.Web/Program.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apps/Elsa.Server.Web/Program.cs b/src/apps/Elsa.Server.Web/Program.cs index 526cd58b0..285d02f23 100644 --- a/src/apps/Elsa.Server.Web/Program.cs +++ b/src/apps/Elsa.Server.Web/Program.cs @@ -48,10 +48,12 @@ using Elsa.Tenants.Extensions; using Elsa.Workflows; using Elsa.Workflows.Api; using Elsa.Workflows.CommitStates.Strategies; +using Elsa.Workflows.IncidentStrategies; using Elsa.Workflows.LogPersistence; using Elsa.Workflows.Management; using Elsa.Workflows.Management.Compression; using Elsa.Workflows.Management.Stores; +using Elsa.Workflows.Options; using Elsa.Workflows.Runtime.Distributed.Extensions; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Stores; @@ -687,8 +689,9 @@ services.Configure(options => }); services.Configure(options => options.Ttl = TimeSpan.FromSeconds(10)); - services.Configure(options => options.CacheDuration = TimeSpan.FromDays(1)); +services.Configure(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy)); + services.AddHealthChecks(); services.AddControllers(); services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*"))); From 046b88f5d283f71e299925e6e89038456ad40ca1 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 2 Mar 2025 11:17:18 +0100 Subject: [PATCH 2/5] 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 From 549b4709d02d2156628ea8bd3f4286c4b03586af Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 2 Mar 2025 11:22:03 +0100 Subject: [PATCH 3/5] Add functions to get and set workflow instance name Introduce `getWorkflowInstanceName` and `setWorkflowInstanceName` functions to the scripting engine. This enables retrieving and updating the workflow instance name dynamically during script execution. Additionally, the related function definitions have been added to the provider. --- .../Handlers/ConfigureEngineWithCommonFunctions.cs | 2 ++ .../Providers/CommonFunctionsDefinitionProvider.cs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs index 3c9295288..0af980344 100644 --- a/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs +++ b/src/modules/Elsa.JavaScript/Handlers/ConfigureEngineWithCommonFunctions.cs @@ -35,6 +35,8 @@ public class ConfigureEngineWithCommonFunctions(IOptions options) : engine.SetValue("getWorkflowInstanceId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id)); engine.SetValue("setCorrelationId", (Action)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value)); engine.SetValue("getCorrelationId", (Func)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId)); + engine.SetValue("setWorkflowInstanceName", (Action)(value => context.GetWorkflowExecutionContext().Name = value)); + engine.SetValue("getWorkflowInstanceName", (Func)(() => context.GetWorkflowExecutionContext().Name)); engine.SetValue("setVariable", (Action)((name, value) => { engine.SyncVariablesContainer(options, name, value); diff --git a/src/modules/Elsa.JavaScript/Providers/CommonFunctionsDefinitionProvider.cs b/src/modules/Elsa.JavaScript/Providers/CommonFunctionsDefinitionProvider.cs index 32f567e6a..76598428d 100644 --- a/src/modules/Elsa.JavaScript/Providers/CommonFunctionsDefinitionProvider.cs +++ b/src/modules/Elsa.JavaScript/Providers/CommonFunctionsDefinitionProvider.cs @@ -41,6 +41,14 @@ internal class CommonFunctionsDefinitionProvider(ITypeAliasRegistry typeAliasReg yield return CreateFunctionDefinition(builder => builder .Name("setCorrelationId") .Parameter("value", "string")); + + yield return CreateFunctionDefinition(builder => builder + .Name("getWorkflowInstanceName") + .ReturnType("string")); + + yield return CreateFunctionDefinition(builder => builder + .Name("setWorkflowInstanceName") + .Parameter("value", "string")); yield return CreateFunctionDefinition(builder => builder .Name("setVariable") From 27fe72d123339ae9a484f2820d4ba1dc8fd6eacb Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 2 Mar 2025 11:24:21 +0100 Subject: [PATCH 4/5] Add WorkflowInstanceName property to Globals Introduces a new property, `WorkflowInstanceName`, to access and modify the name of the current workflow instance. Also simplifies object initialization by using target-typed new expressions. Annotates the `Globals` class with `[UsedImplicitly]` to improve code analysis. --- src/modules/Elsa.CSharp/Models/Globals.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/modules/Elsa.CSharp/Models/Globals.cs b/src/modules/Elsa.CSharp/Models/Globals.cs index f86b9693d..cc3de6dc2 100644 --- a/src/modules/Elsa.CSharp/Models/Globals.cs +++ b/src/modules/Elsa.CSharp/Models/Globals.cs @@ -1,11 +1,13 @@ using Elsa.Expressions.Models; using Elsa.Extensions; +using JetBrains.Annotations; namespace Elsa.CSharp.Models; /// /// Provides access to global objects, such as the workflow execution context. /// +[UsedImplicitly] public partial class Globals { /// @@ -15,9 +17,9 @@ public partial class Globals { ExpressionExecutionContext = expressionExecutionContext; Arguments = arguments; - ExecutionContext = new ExecutionContextProxy(expressionExecutionContext); - Output = new OutputProxy(expressionExecutionContext); - Outcome = new OutcomeProxy(expressionExecutionContext); + ExecutionContext = new(expressionExecutionContext); + Output = new(expressionExecutionContext); + Outcome = new(expressionExecutionContext); } /// @@ -48,6 +50,15 @@ public partial class Globals get => ExpressionExecutionContext.GetWorkflowExecutionContext().CorrelationId; set => ExpressionExecutionContext.GetWorkflowExecutionContext().CorrelationId = value; } + + /// + /// Gets or sets the name of the current workflow instance. + /// + public string? WorkflowInstanceName + { + get => ExpressionExecutionContext.GetWorkflowExecutionContext().Name; + set => ExpressionExecutionContext.GetWorkflowExecutionContext().Name = value; + } /// /// Gets additional arguments provided by the caller of the evaluator. From 0ceeb801e7dc0d71cbdd63c26cb3f61824d91d77 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Sun, 2 Mar 2025 11:28:56 +0100 Subject: [PATCH 5/5] Add support for naming workflow instances during execution This update introduces a `Name` property to workflow execution requests, allowing workflows to be named when started. The changes ensure the `Name` is propagated through the relevant services and endpoints, improving tracking and identification of workflow instances. --- .../WorkflowDefinitions/Execute/EndpointBase.cs | 1 + .../Endpoints/WorkflowDefinitions/Execute/Models.cs | 11 +++++++---- .../Requests/StartWorkflowRequest.cs | 5 +++++ .../Services/DefaultWorkflowStarter.cs | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs index d5a77fc6a..4ca3e37ef 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/EndpointBase.cs @@ -43,6 +43,7 @@ internal abstract class EndpointBase( { Workflow = workflowGraph.Workflow, CorrelationId = request.CorrelationId, + Name = request.Name, Input = request.GetInputAsDictionary(), TriggerActivityId = request.TriggerActivityId, ActivityHandle = request.ActivityHandle diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs index 12b31ad04..51f115e2f 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowDefinitions/Execute/Models.cs @@ -13,6 +13,7 @@ public interface IExecutionRequest { string DefinitionId { get; } string? CorrelationId { get; } + string? Name { get; } string? TriggerActivityId { get; } ActivityHandle? ActivityHandle { get; } VersionOptions? VersionOptions { get; } @@ -22,8 +23,9 @@ public interface IExecutionRequest public class PostRequest : IExecutionRequest { - public string DefinitionId { get; set; } = default!; + public string DefinitionId { get; set; } = null!; public string? CorrelationId { get; set; } + public string? Name { get; set; } public string? TriggerActivityId { get; set; } public ActivityHandle? ActivityHandle { get; set; } public VersionOptions? VersionOptions { get; set; } @@ -36,8 +38,9 @@ public class PostRequest : IExecutionRequest public class GetRequest : IExecutionRequest { - public string DefinitionId { get; set; } = default!; + public string DefinitionId { get; set; } = null!; public string? CorrelationId { get; set; } + public string? Name { get; set; } public string? TriggerActivityId { get; set; } public ActivityHandle? ActivityHandle { get; set; } public VersionOptions? VersionOptions { get; set; } @@ -45,9 +48,9 @@ public class GetRequest : IExecutionRequest public IDictionary? GetInputAsDictionary() { - var result = Input?.TryConvertTo(new ObjectConverterOptions + var result = Input?.TryConvertTo(new() { - SerializerOptions = new JsonSerializerOptions + SerializerOptions = new() { Converters = { new ExpandoObjectConverter() } } diff --git a/src/modules/Elsa.Workflows.Runtime/Requests/StartWorkflowRequest.cs b/src/modules/Elsa.Workflows.Runtime/Requests/StartWorkflowRequest.cs index 216b4b8e3..c8ca5d215 100644 --- a/src/modules/Elsa.Workflows.Runtime/Requests/StartWorkflowRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Requests/StartWorkflowRequest.cs @@ -22,6 +22,11 @@ public class StartWorkflowRequest /// public string? CorrelationId { get; set; } + /// + /// The name to use when starting a new workflow instance. + /// + public string? Name { get; set; } + /// /// The input to the workflow instance, if any. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs index db557bbbc..96b2460f1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultWorkflowStarter.cs @@ -29,6 +29,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio { WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflow.Identity.Id), CorrelationId = request.CorrelationId, + Name = request.Name, Input = request.Input, TriggerActivityId = request.TriggerActivityId, ActivityHandle = request.ActivityHandle,