From 8e347a8ae62aec24916e4bf070e0b9b7e977531b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 1 Aug 2025 20:53:48 +0200 Subject: [PATCH] Refactor activity execution record capturing Replaced `CaptureActivityExecutionRecordMiddleware` with a notification-based approach using `ActivityCompleted` and `CaptureActivityExecutionState`. Removed obsolete middleware setup and extensions for better maintainability. --- .../ScheduledChildCallbackBehavior.cs | 2 +- .../ActivityExecutionContext.Complete.cs | 4 +++ .../Notifications/ActivityCompleted.cs | 5 +++ .../Signals/ActivityCompleted.cs | 2 +- .../Features/DistributedRuntimeFeature.cs | 3 +- ...ctivityExecutionContextRecordExtensions.cs | 32 +++++++++---------- .../PipelineWorkflowsFeatureExtensions.cs | 1 - .../Features/WorkflowRuntimeFeature.cs | 1 + .../Handlers/CancelWorkflowsCommandHandler.cs | 2 ++ .../Handlers/CaptureActivityExecutionState.cs | 19 +++++++++++ ...ivityExecutionPipelineBuilderExtensions.cs | 8 ----- ...aptureActivityExecutionRecordMiddleware.cs | 13 -------- 12 files changed, 49 insertions(+), 43 deletions(-) create mode 100644 src/modules/Elsa.Workflows.Core/Notifications/ActivityCompleted.cs create mode 100644 src/modules/Elsa.Workflows.Runtime/Handlers/CaptureActivityExecutionState.cs delete mode 100644 src/modules/Elsa.Workflows.Runtime/Middleware/Activities/CaptureActivityExecutionRecordMiddleware.cs diff --git a/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs b/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs index 709f58cfd..b0d53337c 100644 --- a/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs +++ b/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs @@ -1,7 +1,7 @@ using Elsa.Mediator.Contracts; using Elsa.Workflows.Notifications; -using Elsa.Workflows.Signals; using JetBrains.Annotations; +using ActivityCompleted = Elsa.Workflows.Signals.ActivityCompleted; namespace Elsa.Workflows.Behaviors; diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs index 5379093ff..faa250d04 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.Complete.cs @@ -1,4 +1,5 @@ using Elsa.Extensions; +using Elsa.Mediator.Contracts; using Elsa.Workflows.Activities.Flowchart.Models; using Elsa.Workflows.Signals; @@ -62,6 +63,9 @@ public partial class ActivityExecutionContext // Update the completed at timestamp. CompletedAt = WorkflowExecutionContext.SystemClock.UtcNow; + + var mediator = GetRequiredService(); + await mediator.SendAsync(new Notifications.ActivityCompleted(this), CancellationToken); } /// diff --git a/src/modules/Elsa.Workflows.Core/Notifications/ActivityCompleted.cs b/src/modules/Elsa.Workflows.Core/Notifications/ActivityCompleted.cs new file mode 100644 index 000000000..0d018f2d2 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Notifications/ActivityCompleted.cs @@ -0,0 +1,5 @@ +using Elsa.Mediator.Contracts; + +namespace Elsa.Workflows.Notifications; + +public record ActivityCompleted(ActivityExecutionContext ActivityExecutionContext) : INotification; \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs b/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs index 894af1256..83b5a78f5 100644 --- a/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs +++ b/src/modules/Elsa.Workflows.Core/Signals/ActivityCompleted.cs @@ -4,4 +4,4 @@ namespace Elsa.Workflows.Signals; /// Signaled when an activity has completed. /// /// An optional result. -public record ActivityCompleted(object? Result = default); \ No newline at end of file +public record ActivityCompleted(object? Result = null); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs index 0e05eca4a..5232c4302 100644 --- a/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime.Distributed/Features/DistributedRuntimeFeature.cs @@ -33,7 +33,6 @@ public class DistributedRuntimeFeature : FeatureBase { Services .AddScoped() - .AddScoped() - .AddCommandHandler(); + .AddScoped(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionContextRecordExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionContextRecordExtensions.cs index b9a49106c..b0db9f1c5 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionContextRecordExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionContextRecordExtensions.cs @@ -8,34 +8,32 @@ namespace Elsa.Extensions; public static class ActivityExecutionContextRecordExtensions { private const string ActivityExecutionRecordKey = "CapturedActivityExecutionRecord"; - + + /// + /// Captures the activity execution record for the provided and stores it in the context's transient properties. + /// public static async Task CaptureActivityExecutionRecordAsync(this ActivityExecutionContext context) { var mapper = context.GetRequiredService(); var record = await mapper.MapAsync(context); context.TransientProperties[ActivityExecutionRecordKey] = record; } - + + /// + /// Retrieves the captured activity execution record from the transient properties of the provided . + /// If the record is not found, it maps and returns a new activity execution record using the service. + /// public static async Task GetOrMapCapturedActivityExecutionRecordAsync(this ActivityExecutionContext context) { + // If the record is already captured in the transient properties, return it, as it will contain the serialized state of the activity execution at the time of capture, rather than the current state. + // This is useful for scenarios where the activity execution state may change after the record is captured, such as referenced workflow variables. + if (context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var capturedRecord)) + return (ActivityExecutionRecord)capturedRecord; + + // If the record is not captured, map a new activity execution record using the mapper. var mapper = context.GetRequiredService(); var record = await mapper.MapAsync(context); - if (context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var capturedRecord)) - { - var serializedSnapshot = ((ActivityExecutionRecord)capturedRecord).SerializedSnapshot!; - - // Take the existing serialized snapshot. - record.SerializedSnapshot = serializedSnapshot; - - // Update the serialized snapshot with the current record's properties. - // This will reflect the latest state of the activity execution context without losing the existing serialized snapshot representing e.g., variable values at the time of the record capture. - serializedSnapshot.HasBookmarks = record.HasBookmarks; - serializedSnapshot.Status = record.Status; - serializedSnapshot.AggregateFaultCount = record.AggregateFaultCount; - serializedSnapshot.CompletedAt = record.CompletedAt; - } - return record; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs index 0224b0fb4..642e449eb 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/PipelineWorkflowsFeatureExtensions.cs @@ -34,7 +34,6 @@ public static class PipelineWorkflowsFeatureExtensions .UseExecutionLogging() .UseNotifications() .UseLogPersistenceModeEvaluation() - .UseActivityExecutionLogCapturing() .UseBackgroundActivityInvoker(); configurePipeline?.Invoke(pipeline); diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index 7566d57a0..d8f57b739 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -345,6 +345,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module) .AddNotificationHandler() .AddNotificationHandler() .AddNotificationHandler() + .AddNotificationHandler() // Workflow activation strategies. .AddScoped() diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs index 776db86c9..5be9522e6 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/CancelWorkflowsCommandHandler.cs @@ -1,12 +1,14 @@ using Elsa.Mediator.Contracts; using Elsa.Mediator.Models; using Elsa.Workflows.Runtime.Commands; +using JetBrains.Annotations; namespace Elsa.Workflows.Runtime.Handlers; /// /// Handles the . /// +[UsedImplicitly] public class CancelWorkflowsCommandHandler(IWorkflowRuntime workflowRuntime) : ICommandHandler { /// diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/CaptureActivityExecutionState.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/CaptureActivityExecutionState.cs new file mode 100644 index 000000000..7b9b8de25 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/CaptureActivityExecutionState.cs @@ -0,0 +1,19 @@ +using Elsa.Extensions; +using Elsa.Mediator.Contracts; +using Elsa.Workflows.Notifications; +using JetBrains.Annotations; + +namespace Elsa.Workflows.Runtime.Handlers; + +/// +/// Captures the execution state of an activity when it completes. +/// +[UsedImplicitly] +public class CaptureActivityExecutionState : INotificationHandler +{ + public async Task HandleAsync(ActivityCompleted notification, CancellationToken cancellationToken) + { + var context = notification.ActivityExecutionContext; + await context.CaptureActivityExecutionRecordAsync(); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/LogPersistence/Extensions/ActivityExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/LogPersistence/Extensions/ActivityExecutionPipelineBuilderExtensions.cs index b57f15df0..988b6d87b 100644 --- a/src/modules/Elsa.Workflows.Runtime/LogPersistence/Extensions/ActivityExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/LogPersistence/Extensions/ActivityExecutionPipelineBuilderExtensions.cs @@ -20,12 +20,4 @@ public static class ActivityExecutionPipelineBuilderExtensions /// Installs the which evaluates log persistence modes during activity execution. /// public static IActivityExecutionPipelineBuilder UseLogPersistenceModeEvaluation(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); - - /// - /// Installs the into the activity execution pipeline to capture and map activity execution details. - /// - public static IActivityExecutionPipelineBuilder UseActivityExecutionLogCapturing(this IActivityExecutionPipelineBuilder pipelineBuilder) - { - return pipelineBuilder.UseMiddleware(); - } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/CaptureActivityExecutionRecordMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/CaptureActivityExecutionRecordMiddleware.cs deleted file mode 100644 index 986a05ef7..000000000 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/CaptureActivityExecutionRecordMiddleware.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Elsa.Extensions; -using Elsa.Workflows.Pipelines.ActivityExecution; - -namespace Elsa.Workflows.Runtime.Middleware.Activities; - -public class CaptureActivityExecutionRecordMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware -{ - public async ValueTask InvokeAsync(ActivityExecutionContext context) - { - await next(context); - await context.CaptureActivityExecutionRecordAsync(); - } -} \ No newline at end of file