diff --git a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs index 4bc70d80c..f846e499e 100644 --- a/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs +++ b/src/clients/Elsa.Api.Client/Extensions/DependencyInjectionExtensions.cs @@ -12,6 +12,7 @@ using Elsa.Api.Client.Resources.Resilience.Contracts; using Elsa.Api.Client.Resources.Scripting.Contracts; using Elsa.Api.Client.Resources.StorageDrivers.Contracts; using Elsa.Api.Client.Resources.Tasks.Contracts; +using Elsa.Api.Client.Resources.Tests; using Elsa.Api.Client.Resources.VariableTypes.Contracts; using Elsa.Api.Client.Resources.WorkflowActivationStrategies.Contracts; using Elsa.Api.Client.Resources.WorkflowDefinitions.Contracts; @@ -86,6 +87,7 @@ public static class DependencyInjectionExtensions services.AddApi(builderOptions); services.AddApi(builderOptions); services.AddApi(builderOptions); + services.AddApi(builderOptions); }); } diff --git a/src/clients/Elsa.Api.Client/Resources/Tests/ITestsApi.cs b/src/clients/Elsa.Api.Client/Resources/Tests/ITestsApi.cs new file mode 100644 index 000000000..09748d7e1 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Tests/ITestsApi.cs @@ -0,0 +1,18 @@ +using Refit; + +namespace Elsa.Api.Client.Resources.Tests; + +/// +/// Represents a client for the testing API. +/// +public interface ITestsApi +{ + /// + /// Sends the specified request to the login API. + /// + /// The request. + /// The cancellation token. + /// The response. + [Post("/tests/activities")] + Task TestActivityAsync([Body] TestActivityRequest request, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Tests/TestActivityRequest.cs b/src/clients/Elsa.Api.Client/Resources/Tests/TestActivityRequest.cs new file mode 100644 index 000000000..49dfe56d0 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Tests/TestActivityRequest.cs @@ -0,0 +1,9 @@ +using Elsa.Api.Client.Resources.WorkflowDefinitions.Models; + +namespace Elsa.Api.Client.Resources.Tests; + +public class TestActivityRequest +{ + public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!; + public ActivityHandle ActivityHandle { get; set; } = null!; +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/Tests/TestActivityResponse.cs b/src/clients/Elsa.Api.Client/Resources/Tests/TestActivityResponse.cs new file mode 100644 index 000000000..2830e7431 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/Tests/TestActivityResponse.cs @@ -0,0 +1,12 @@ +using Elsa.Api.Client.Resources.WorkflowInstances.Models; +using Elsa.Api.Client.Shared.Models; + +namespace Elsa.Api.Client.Resources.Tests; + +public class TestActivityResponse +{ + public IDictionary? Outputs { get; set; } + public ICollection? Outcomes { get; set; } + public ExceptionState? Exception { get; set; } + public ActivityStatus Status { get; set; } +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityHandle.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityHandle.cs new file mode 100644 index 000000000..c6ee9fd16 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/ActivityHandle.cs @@ -0,0 +1,16 @@ +namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; + +/// +/// Represents a handle to an activity. +/// +public class ActivityHandle +{ + public static ActivityHandle FromActivityId(string activityId) => new() { ActivityId = activityId }; + public static ActivityHandle FromActivityNodeId(string activityNodeId) => new() { ActivityNodeId = activityNodeId }; + public static ActivityHandle FromActivityInstanceId(string activityInstanceId) => new() { ActivityInstanceId = activityInstanceId }; + public static ActivityHandle FromActivityHash(string activityHash) => new() { ActivityHash = activityHash }; + public string? ActivityId { get; init; } + public string? ActivityNodeId { get; init;} + public string? ActivityInstanceId { get; init;} + public string? ActivityHash { get; init;} +} \ No newline at end of file diff --git a/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionHandle.cs b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionHandle.cs new file mode 100644 index 000000000..59f337784 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/WorkflowDefinitions/Models/WorkflowDefinitionHandle.cs @@ -0,0 +1,47 @@ +using Elsa.Api.Client.Shared.Models; + +namespace Elsa.Api.Client.Resources.WorkflowDefinitions.Models; + +/// +/// Represents a handle to a workflow definition. +/// +public class WorkflowDefinitionHandle +{ + /// + /// Gets or sets the definition ID. When set, the property is ignored. + /// + public string? DefinitionId { get; set; } + + /// + /// Gets or sets the version options. When set, the property is ignored. + /// + public VersionOptions? VersionOptions { get; set; } + + /// + /// Gets or sets the definition version ID. When set, the and properties are ignored. + /// + public string? DefinitionVersionId { get; set; } + + /// + /// Creates a new instance with the specified definition ID and version options. + /// + public static WorkflowDefinitionHandle ByDefinitionId(string definitionId, VersionOptions? versionOptions = null) => new() { DefinitionId = definitionId, VersionOptions = versionOptions }; + + /// + /// Creates a new instance with the specified definition version ID. + /// + /// + public static WorkflowDefinitionHandle ByDefinitionVersionId(string definitionVersionId) => new() { DefinitionVersionId = definitionVersionId }; + + /// + public override string ToString() + { + if (DefinitionId != null) + return $"DefinitionId: {DefinitionId}, VersionOptions: {VersionOptions}"; + + if (DefinitionVersionId != null) + return $"DefinitionVersionId: {DefinitionVersionId}"; + + return string.Empty; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Expressions.Liquid/Handlers/ConfigureLiquidEngine.cs b/src/modules/Elsa.Expressions.Liquid/Handlers/ConfigureLiquidEngine.cs index 8c2746ddc..34bff8f32 100644 --- a/src/modules/Elsa.Expressions.Liquid/Handlers/ConfigureLiquidEngine.cs +++ b/src/modules/Elsa.Expressions.Liquid/Handlers/ConfigureLiquidEngine.cs @@ -91,29 +91,11 @@ internal class ConfigureLiquidEngine : INotificationHandler EnumerateVariablesInScope(ExpressionExecutionContext context) - { - var currentScope = context; - - while (currentScope != null) - { - if (!currentScope.TryGetActivityExecutionContext(out var activityExecutionContext)) - break; - - var variables = activityExecutionContext.Variables; - - foreach (var variable in variables) - yield return variable; - - currentScope = currentScope.ParentContext; - } - } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/Tests/Activities/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/Tests/Activities/Endpoint.cs new file mode 100644 index 000000000..12311231f --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/Tests/Activities/Endpoint.cs @@ -0,0 +1,83 @@ +using Elsa.Abstractions; +using Elsa.Workflows.Management; +using Elsa.Workflows.Models; +using Elsa.Workflows.Runtime; +using Elsa.Workflows.State; + +namespace Elsa.Workflows.Api.Endpoints.Tests.Activities; + +/// +/// Represents an endpoint for testing activities in workflows. This endpoint is responsible for handling requests +/// that test the execution of a specific activity in a workflow and returning the results. +/// +/// +/// This endpoint is used to perform operations such as: +/// - Finding a workflow graph based on a provided workflow definition handle. +/// - Locating and executing a specific activity within the workflow graph. +/// - Capturing the execution results and returning them as a response. +/// +internal class Endpoint( + IWorkflowDefinitionService workflowDefinitionService, + IActivityTestRunner activityTestRunner, + IActivityExecutionMapper activityExecutionMapper) + : ElsaEndpoint +{ + /// + public override void Configure() + { + Post("/tests/activities"); + ConfigurePermissions("exec:tests"); + } + + /// + public override async Task HandleAsync(Request request, CancellationToken cancellationToken) + { + var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(request.WorkflowDefinitionHandle, cancellationToken); + + if (workflowGraph == null) + { + AddError("Workflow definition not found."); + await SendErrorsAsync(cancellation: cancellationToken); + return; + } + + var activity = workflowGraph.FindActivity(request.ActivityHandle); + + if (activity == null) + { + AddError("Activity not found."); + await SendErrorsAsync(cancellation: cancellationToken); + return; + } + + var activityExecutionContext = await activityTestRunner.RunAsync(workflowGraph, activity, cancellationToken); + var record = activityExecutionMapper.Map(activityExecutionContext); + var activityState = record.ActivityState ?? new Dictionary(); + + var response = new Response + { + ActivityState = activityState, + Outputs = record.Outputs, + Payload = record.Payload, + Exception = record.Exception, + Status = record.Status + }; + + await SendOkAsync(response, cancellationToken); + } +} + +public class Request +{ + public WorkflowDefinitionHandle WorkflowDefinitionHandle { get; set; } = null!; + public ActivityHandle ActivityHandle { get; set; } = null!; +} + +public class Response +{ + public IDictionary ActivityState { get; set; } = null!; + public IDictionary? Outputs { get; set; } + public IDictionary? Payload { get; set; } + public ExceptionState? Exception { get; set; } + public ActivityStatus Status { get; set; } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index b08a72c74..af08c5b40 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -232,7 +232,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable /// /// Returns the global node ID for the current activity within the graph. /// - /// As of tool version 3.0, all activity Ids are already unique, so there's no need to construct a hierarchical ID + /// As of tool version 3.0, all activity IDs are already unique, so there's no need to construct a hierarchical ID public string NodeId => ActivityNode.NodeId; public ISet Children { get; } = new HashSet(); diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 0254a95bb..262d327c1 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -85,6 +85,28 @@ public partial class WorkflowExecutionContext : IExecutionContext _cancellationTokenSources.Add(linkedCancellationTokenSource); _cancellationRegistrations.Add(linkedCancellationTokenSource.Token.Register(CancelWorkflow)); } + + /// + /// Creates a new for the specified workflow. + /// + public static async Task CreateAsync( + IServiceProvider serviceProvider, + WorkflowGraph workflowGraph, + string id, + CancellationToken cancellationToken = default) + { + var systemClock = serviceProvider.GetRequiredService(); + + return await CreateAsync( + serviceProvider, + workflowGraph, + id, + new List(), + new List(), + systemClock.UtcNow, + cancellationToken: cancellationToken + ); + } /// /// Creates a new for the specified workflow. diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityInvoker.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityInvoker.cs index a3cd9af2f..6b9267cc8 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityInvoker.cs @@ -13,7 +13,7 @@ public interface IActivityInvoker /// The workflow execution context. /// The activity to invoke. /// - Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = default); + Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null); /// /// Invokes the specified activity execution context. diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IActivityTestRunner.cs b/src/modules/Elsa.Workflows.Core/Contracts/IActivityTestRunner.cs new file mode 100644 index 000000000..299ae2278 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Contracts/IActivityTestRunner.cs @@ -0,0 +1,8 @@ +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +public interface IActivityTestRunner +{ + Task RunAsync(WorkflowGraph workflowGraph, IActivity activity, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs index 5b3c7fd0e..a5bf9f801 100644 --- a/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Contracts/IWorkflowRunner.cs @@ -10,14 +10,14 @@ namespace Elsa.Workflows; /// public interface IWorkflowRunner { - Task RunAsync(IActivity activity, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); - Task RunAsync(IWorkflow workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); - Task> RunAsync(WorkflowBase workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); - Task RunAsync(RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) where T : IWorkflow, new(); - Task RunAsync(RunWorkflowOptions? options = default, CancellationToken cancellationToken = default) where T : WorkflowBase, new(); - Task RunAsync(Workflow workflow, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); - Task RunAsync(Workflow workflow, WorkflowState workflowState, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); - Task RunAsync(WorkflowGraph workflowGraph, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); - Task RunAsync(WorkflowGraph workflowGraph, WorkflowState workflowState, RunWorkflowOptions? options = default, CancellationToken cancellationToken = default); + Task RunAsync(IActivity activity, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); + Task RunAsync(IWorkflow workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); + Task> RunAsync(WorkflowBase workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); + Task RunAsync(RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) where T : IWorkflow, new(); + Task RunAsync(RunWorkflowOptions? options = null, CancellationToken cancellationToken = default) where T : WorkflowBase, new(); + Task RunAsync(Workflow workflow, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); + Task RunAsync(Workflow workflow, WorkflowState workflowState, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); + Task RunAsync(WorkflowGraph workflowGraph, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); + Task RunAsync(WorkflowGraph workflowGraph, WorkflowState workflowState, RunWorkflowOptions? options = null, CancellationToken cancellationToken = default); Task RunAsync(WorkflowExecutionContext workflowExecutionContext); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs index cc3f81aad..655bdaf5a 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -163,6 +163,7 @@ public class WorkflowsFeature : FeatureBase // Core. .AddScoped() .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index a4eb9ba8e..6731bb8a6 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -82,14 +82,6 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I // Invoke next middleware. await next(context); - // // If the activity created any bookmarks, copy them into the workflow execution context. - // if (context.Bookmarks.Any()) - // { - // // Store bookmarks. - // workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks); - // logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count); - // } - // Conditionally commit the workflow state. if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuted)) await context.WorkflowExecutionContext.CommitAsync(); diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowDefinitionHandle.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowDefinitionHandle.cs index 92957f25e..4844ec0f3 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowDefinitionHandle.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowDefinitionHandle.cs @@ -25,7 +25,7 @@ public class WorkflowDefinitionHandle /// /// Creates a new instance with the specified definition ID and version options. /// - public static WorkflowDefinitionHandle ByDefinitionId(string definitionId, VersionOptions? versionOptions = default) => new() { DefinitionId = definitionId, VersionOptions = versionOptions }; + public static WorkflowDefinitionHandle ByDefinitionId(string definitionId, VersionOptions? versionOptions = null) => new() { DefinitionId = definitionId, VersionOptions = versionOptions }; /// /// Creates a new instance with the specified definition version ID. diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowGraph.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowGraph.cs index d8232a8bb..946be40ee 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowGraph.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowGraph.cs @@ -53,6 +53,54 @@ public record WorkflowGraph /// public IDictionary NodeIdLookup { get; } + /// + /// Finds the activity based on the provided . + /// + /// The handle containing the identification parameters for the activity. + /// The activity found based on the handle, or null if no activity is found. + public IActivity? FindActivity(ActivityHandle handle) + { + return handle.ActivityId != null + ? FindActivityById(handle.ActivityId) + : handle.ActivityNodeId != null + ? FindActivityByNodeId(handle.ActivityNodeId) + : handle.ActivityHash != null + ? FindActivityByHash(handle.ActivityHash) + : null; + } + + /// + /// Returns the with the specified activity ID from the workflow graph. + /// + public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : null; + + /// + /// Returns the with the specified hash of the activity node ID from the workflow graph. + /// + /// The hash of the activity node ID. + /// The with the specified hash of the activity node ID. + public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : null; + + /// Returns the containing the specified activity from the workflow graph. + public ActivityNode? FindNodeByActivity(IActivity activity) + { + return NodeActivityLookup.TryGetValue(activity, out var node) ? node : null; + } + + /// Returns the associated with the specified activity ID. + public ActivityNode? FindNodeByActivityId(string activityId) => Nodes.FirstOrDefault(x => x.Activity.Id == activityId); + + /// Returns the with the specified ID from the workflow graph. + public IActivity? FindActivityByNodeId(string nodeId) => FindNodeById(nodeId)?.Activity; + + /// Returns the with the specified ID from the workflow graph. + public IActivity? FindActivityById(string activityId) => FindNodeById(NodeIdLookup.SingleOrDefault(n => n.Key.EndsWith(activityId)).Value.NodeId)?.Activity; + + /// Returns the with the specified hash of the activity node ID from the workflow graph. + /// The hash of the activity node ID. + /// The with the specified hash of the activity node ID. + public IActivity? FindActivityByHash(string hash) => FindNodeByHash(hash)?.Activity; + private static string Hash(HashAlgorithm hashAlgorithm, string input) { var data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input)); diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs index a7d86aa5d..4560f6974 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityInvoker.cs @@ -12,7 +12,7 @@ public class ActivityInvoker( { /// - public async Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null) + public async Task InvokeAsync(WorkflowExecutionContext workflowExecutionContext, IActivity activity, ActivityInvocationOptions? options = null) { // Setup an activity execution context, potentially reusing an existing one if requested. var existingActivityExecutionContext = options?.ExistingActivityExecutionContext; @@ -34,6 +34,8 @@ public class ActivityInvoker( // Execute the activity execution pipeline. await InvokeAsync(activityExecutionContext); + + return activityExecutionContext; } /// diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityTestRunner.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityTestRunner.cs new file mode 100644 index 000000000..f0d41edf8 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityTestRunner.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using Elsa.Extensions; +using Elsa.Workflows.Models; + +namespace Elsa.Workflows; + +/// +public class ActivityTestRunner( + IServiceProvider serviceProvider, + IWorkflowExecutionPipeline pipeline, + IIdentityGenerator identityGenerator) + : IActivityTestRunner +{ + /// + public async Task RunAsync(WorkflowGraph workflowGraph, IActivity activity, CancellationToken cancellationToken = default) + { + var id = identityGenerator.GenerateId(); + var workflowExecutionContext = await WorkflowExecutionContext.CreateAsync(serviceProvider, workflowGraph, id, cancellationToken); + var variableTestValues = GetVariableTestValues(workflowGraph); + + foreach (var variable in workflowGraph.Workflow.Variables) + { + var variableValue = variableTestValues.TryGetValue(variable.Id, out var value) ? value : variable.Value; + variable.Set(workflowExecutionContext.ExpressionExecutionContext!, variableValue); + } + + workflowExecutionContext.ScheduleActivity(activity); + workflowExecutionContext.TransitionTo(WorkflowSubStatus.Executing); + + await pipeline.ExecuteAsync(workflowExecutionContext); + var activityExecutionContext = workflowExecutionContext + .ActivityExecutionContexts + .First(x => x.Activity == activity); + return activityExecutionContext; + } + + private IDictionary GetVariableTestValues(WorkflowGraph workflowGraph) + { + var variableTestValues = workflowGraph.Workflow.CustomProperties.TryGetValue("VariableTestValues", out var variableTestValuesObj) ? variableTestValuesObj : null; + + if (variableTestValues is JsonElement jsonElement) + variableTestValues = JsonSerializer.Deserialize>(jsonElement.GetRawText()); + + return variableTestValues as IDictionary ?? new Dictionary(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs b/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs index f3802e958..d465629d8 100644 --- a/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs +++ b/src/modules/Elsa.Workflows.Management/Handlers/Notifications/EvictWorkflowDefinitionServiceCache.cs @@ -12,28 +12,34 @@ namespace Elsa.Workflows.Management.Handlers.Notifications; /// [UsedImplicitly] internal class EvictWorkflowDefinitionServiceCache(IWorkflowDefinitionCacheManager workflowDefinitionCacheManager) : + INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler { - /// - public async Task HandleAsync(WorkflowDefinitionPublishing notification, CancellationToken cancellationToken) + public Task HandleAsync(WorkflowDefinitionDraftSaving notification, CancellationToken cancellationToken) { - await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken); + return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken); + } + + /// + public Task HandleAsync(WorkflowDefinitionPublishing notification, CancellationToken cancellationToken) + { + return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken); } /// - public async Task HandleAsync(WorkflowDefinitionRetracting notification, CancellationToken cancellationToken) + public Task HandleAsync(WorkflowDefinitionRetracting notification, CancellationToken cancellationToken) { - await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken); + return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken); } /// - public async Task HandleAsync(WorkflowDefinitionDeleting notification, CancellationToken cancellationToken) + public Task HandleAsync(WorkflowDefinitionDeleting notification, CancellationToken cancellationToken) { - await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.DefinitionId, cancellationToken); + return workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.DefinitionId, cancellationToken); } /// diff --git a/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionDraftSaved.cs b/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionDraftSaved.cs new file mode 100644 index 000000000..b0f6868cb --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionDraftSaved.cs @@ -0,0 +1,12 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Entities; +using JetBrains.Annotations; + +namespace Elsa.Workflows.Management.Notifications; + +/// +/// A notification that is sent when a workflow definition has been updated. +/// +/// The workflow definition. +[PublicAPI] +public record WorkflowDefinitionDraftSaved(WorkflowDefinition WorkflowDefinition) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionDraftSaving.cs b/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionDraftSaving.cs new file mode 100644 index 000000000..a3379dc3e --- /dev/null +++ b/src/modules/Elsa.Workflows.Management/Notifications/WorkflowDefinitionDraftSaving.cs @@ -0,0 +1,12 @@ +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Management.Entities; +using JetBrains.Annotations; + +namespace Elsa.Workflows.Management.Notifications; + +/// +/// A notification that is sent when a workflow definition is being updated. +/// +/// The workflow definition. +[PublicAPI] +public record WorkflowDefinitionDraftSaving(WorkflowDefinition WorkflowDefinition) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs index 88640c69d..0fc3c4d90 100644 --- a/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs +++ b/src/modules/Elsa.Workflows.Management/Services/WorkflowDefinitionPublisher.cs @@ -113,6 +113,7 @@ public class WorkflowDefinitionPublisher( // Save the newly published definition. definition.IsPublished = true; + definition.IsLatest = true; definition = Initialize(definition); await workflowDefinitionStore.SaveAsync(definition, cancellationToken); @@ -194,7 +195,9 @@ public class WorkflowDefinitionPublisher( draft.IsLatest = true; draft = Initialize(draft); + await mediator.SendAsync(new WorkflowDefinitionDraftSaving(draft), cancellationToken); await workflowDefinitionStore.SaveAsync(draft, cancellationToken); + await mediator.SendAsync(new WorkflowDefinitionDraftSaved(draft), cancellationToken); if (lastVersion is null) await mediator.SendAsync(new WorkflowDefinitionCreated(definition), cancellationToken);