Merge pull request #6460 from elsa-workflows/enh/workflowinstance-name

Improved support for named workflow instances
This commit is contained in:
Sipke Schoorstra 2025-03-14 23:03:51 +01:00 committed by GitHub
commit 85eec228cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 162 additions and 45 deletions

View file

@ -47,10 +47,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;
@ -705,7 +707,7 @@ services.Configure<RecurringTaskOptions>(options =>
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(10));
services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));
services.Configure<IncidentOptions>(options => options.DefaultIncidentStrategy = typeof(ContinueWithIncidentsStrategy));
services.AddHealthChecks();
services.AddControllers();
services.AddCors(cors => cors.AddDefaultPolicy(policy => policy.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().WithExposedHeaders("*")));

View file

@ -1,11 +1,13 @@
using Elsa.Expressions.Models;
using Elsa.Extensions;
using JetBrains.Annotations;
namespace Elsa.CSharp.Models;
/// <summary>
/// Provides access to global objects, such as the workflow execution context.
/// </summary>
[UsedImplicitly]
public partial class Globals
{
/// <summary>
@ -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);
}
/// <summary>
@ -48,6 +50,15 @@ public partial class Globals
get => ExpressionExecutionContext.GetWorkflowExecutionContext().CorrelationId;
set => ExpressionExecutionContext.GetWorkflowExecutionContext().CorrelationId = value;
}
/// <summary>
/// Gets or sets the name of the current workflow instance.
/// </summary>
public string? WorkflowInstanceName
{
get => ExpressionExecutionContext.GetWorkflowExecutionContext().Name;
set => ExpressionExecutionContext.GetWorkflowExecutionContext().Name = value;
}
/// <summary>
/// Gets additional arguments provided by the caller of the evaluator.

View file

@ -35,6 +35,8 @@ public class ConfigureEngineWithCommonFunctions(IOptions<JintOptions> options) :
engine.SetValue("getWorkflowInstanceId", (Func<string>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.Id));
engine.SetValue("setCorrelationId", (Action<string?>)(value => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId = value));
engine.SetValue("getCorrelationId", (Func<string?>)(() => context.GetActivityExecutionContext().WorkflowExecutionContext.CorrelationId));
engine.SetValue("setWorkflowInstanceName", (Action<string?>)(value => context.GetWorkflowExecutionContext().Name = value));
engine.SetValue("getWorkflowInstanceName", (Func<string?>)(() => context.GetWorkflowExecutionContext().Name));
engine.SetValue("setVariable", (Action<string, object>)((name, value) =>
{
engine.SyncVariablesContainer(options, name, value);

View file

@ -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")

View file

@ -43,6 +43,7 @@ internal abstract class EndpointBase<T>(
{
Workflow = workflowGraph.Workflow,
CorrelationId = request.CorrelationId,
Name = request.Name,
Input = request.GetInputAsDictionary(),
Variables = request.GetVariablesAsDictionary(),
TriggerActivityId = request.TriggerActivityId,

View file

@ -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; }
@ -25,6 +26,7 @@ public class PostRequest : IExecutionRequest
{
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; }
@ -43,6 +45,7 @@ public class GetRequest : IExecutionRequest
{
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; }

View file

@ -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
{
/// <summary>
/// The property key name used to store the workflow instance name.
/// </summary>
public const string WorkflowInstanceNameKey = "WorkflowInstanceName";
/// <inheritdoc />
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)
{
}
/// <inheritdoc />
public SetName(Input<string> value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
public SetName(Input<string> value, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line)
{
Value = value;
}
@ -36,7 +32,7 @@ public class SetName : CodeActivity
/// <inheritdoc />
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;
}
}

View file

@ -71,8 +71,8 @@ public partial class WorkflowExecutionContext : IExecutionContext
_activityExecutionContexts = new List<ActivityExecutionContext>();
Scheduler = serviceProvider.GetRequiredService<IActivitySchedulerFactory>().CreateScheduler();
IdentityGenerator = serviceProvider.GetRequiredService<IIdentityGenerator>();
Input = input != null ? new Dictionary<string, object>(input, StringComparer.OrdinalIgnoreCase) : new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
Properties = properties != null ? new Dictionary<string, object>(properties, StringComparer.OrdinalIgnoreCase) : new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
Input = input != null ? new(input, StringComparer.OrdinalIgnoreCase) : new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
Properties = properties != null ? new(properties, StringComparer.OrdinalIgnoreCase) : new Dictionary<string, object>(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;
@ -264,6 +264,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; }

View file

@ -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()
};

View file

@ -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,
@ -46,6 +47,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.IsExecuting = state.IsExecuting;

View file

@ -11,17 +11,17 @@ public class WorkflowState
/// <summary>
/// Gets or sets the ID.
/// </summary>
public string Id { get; set; } = default!;
public string Id { get; set; } = null!;
/// <summary>
/// The workflow definition ID.
/// </summary>
public string DefinitionId { get; set; } = default!;
public string DefinitionId { get; set; } = null!;
/// <summary>
/// The workflow definition version ID.
/// </summary>
public string DefinitionVersionId { get; set; } = default!;
public string DefinitionVersionId { get; set; } = null!;
/// <summary>
/// The workflow definition version.
@ -37,6 +37,11 @@ public class WorkflowState
/// The correlation ID of the workflow, if any.
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// Gets or sets the name of the workflow instance.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The status of the workflow.

View file

@ -1,5 +1,3 @@
using Elsa.Extensions;
using Elsa.Workflows.Activities;
using Elsa.Workflows.Management.Entities;
using Elsa.Workflows.State;
@ -23,7 +21,7 @@ public class WorkflowStateMapper
return workflowInstance;
}
/// <summary>
/// Maps a workflow state to a workflow instance.
/// </summary>
@ -39,14 +37,12 @@ public class WorkflowStateMapper
target.SubStatus = source.SubStatus;
target.IsExecuting = source.IsExecuting;
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<string>(SetName.WorkflowInstanceNameKey, out var name))
target.Name = name;
}
/// <summary>
@ -68,13 +64,11 @@ public class WorkflowStateMapper
workflowState.SubStatus = source.SubStatus;
workflowState.IsExecuting = source.IsExecuting;
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;
}
}

View file

@ -13,6 +13,11 @@ public class WorkflowInstanceOptions
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// The name of the workflow instance.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The input to the workflow instance, if any.
/// </summary>

View file

@ -21,6 +21,7 @@ public class WorkflowInstanceFactory(IIdentityGenerator identityGenerator, ISyst
DefinitionVersionId = workflow.Identity.Id,
DefinitionVersion = workflow.Identity.Version,
CorrelationId = options?.CorrelationId,
Name = options?.Name,
Input = options?.Input ?? new Dictionary<string, object>(),
Properties = options?.Properties ?? new Dictionary<string, object>(),
Status = WorkflowStatus.Running,
@ -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,

View file

@ -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),

View file

@ -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()
};

View file

@ -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 {

View file

@ -18,6 +18,11 @@ public class CreateAndRunWorkflowInstanceRequest
/// The correlation ID of the workflow, if any.
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// The name of the workflow instance to be created.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The input to the workflow instance, if any.

View file

@ -12,13 +12,18 @@ public class CreateWorkflowInstanceRequest
/// <summary>
/// The ID of the workflow definition version to create an instance of.
/// </summary>
public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = default!;
public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!;
/// <summary>
/// The correlation ID of the workflow, if any.
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// The name of the workflow instance to be created.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The input to the workflow instance, if any.
/// </summary>

View file

@ -23,6 +23,11 @@ public class StartWorkflowRequest
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// The name to use when starting a new workflow instance.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The input to the workflow instance, if any.
/// </summary>

View file

@ -29,6 +29,7 @@ public class DefaultWorkflowStarter(IWorkflowDefinitionService workflowDefinitio
{
WorkflowDefinitionHandle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflow.Identity.Id),
CorrelationId = request.CorrelationId,
Name = request.Name,
Input = request.Input,
Variables = request.Variables,
TriggerActivityId = request.TriggerActivityId,

View file

@ -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
@ -150,6 +152,7 @@ public class LocalWorkflowClient(
{
WorkflowInstanceId = WorkflowInstanceId,
CorrelationId = request.CorrelationId,
Name = request.Name,
ParentWorkflowInstanceId = request.ParentId,
Input = request.Input,
Properties = request.Properties

View file

@ -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<string>(expectedName));
var result = await _serviceProvider.RunActivityAsync(setName);
var actualName = result.WorkflowState.Properties[SetName.WorkflowInstanceNameKey];
var actualName = result.WorkflowState.Name;
Assert.Equal(expectedName, actualName);
}
}

View file

@ -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)
});

View file

@ -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<NamedWorkflow>()
.Build();
_workflowRuntime = _services.GetRequiredService<IWorkflowRuntime>();
}
[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);
}
}

View file

@ -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);
}
}