diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index e25afabc6..44bf24570 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Contracts; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Contracts; using Elsa.Workflows.Core.Models; @@ -109,4 +110,36 @@ public static class WorkflowExecutionContextExtensions /// Returns true if all activities have completed or canceled, false otherwise. /// public static bool AllActivitiesCompleted(this WorkflowExecutionContext workflowExecutionContext) => workflowExecutionContext.ActiveActivityExecutionContexts.All(x => x.Status != ActivityStatus.Running); + + /// + /// Adds a new to the execution log of the current . + /// + /// The being extended. + /// The name of the event. + /// The message of the event. + /// Any contextual data related to this event. + /// Returns the created . + public static WorkflowExecutionLogEntry AddExecutionLogEntry(this WorkflowExecutionContext context, string eventName, string? message = default, object? payload = default) + { + var now = context.GetRequiredService().UtcNow; + + var logEntry = new WorkflowExecutionLogEntry( + context.Id, + default, + context.Workflow.Id, + context.Workflow.Type, + context.Workflow.Identity.Version, + context.Workflow.Name, + context.Workflow.Identity.Id, + default, + now, + context.ExecutionLogSequence++, + eventName, + message, + context.Workflow.GetSource(), + payload); + + context.ExecutionLog.Add(logEntry); + return logEntry; + } } \ 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 eb6b40b3b..9b107d846 100644 --- a/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs +++ b/src/modules/Elsa.Workflows.Core/Features/WorkflowsFeature.cs @@ -47,7 +47,9 @@ public class WorkflowsFeature : FeatureBase /// /// A delegate to configure the . /// - public Action WorkflowExecutionPipeline { get; set; } = builder => builder.UseDefaultActivityScheduler(); + public Action WorkflowExecutionPipeline { get; set; } = builder => builder + .UseExceptionHandling() + .UseDefaultActivityScheduler(); /// /// A delegate to configure the . diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/ExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/ExceptionHandlingMiddleware.cs new file mode 100644 index 000000000..527c0531f --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/ExceptionHandlingMiddleware.cs @@ -0,0 +1,60 @@ +using Elsa.Common.Contracts; +using Elsa.Extensions; +using Elsa.Workflows.Core.Contracts; +using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Core.Pipelines.WorkflowExecution; +using Elsa.Workflows.Core.State; +using Microsoft.Extensions.Logging; + +namespace Elsa.Workflows.Core.Middleware.Workflows; + +/// +/// Adds extension methods to . +/// +public static class ExceptionHandlingMiddlewareExtensions +{ + /// + /// Installs the component in the activity execution pipeline. + /// + public static IWorkflowExecutionPipelineBuilder UseExceptionHandling(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); +} + +/// +/// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state. +/// +public class ExceptionHandlingMiddleware : IWorkflowExecutionMiddleware +{ + private readonly WorkflowMiddlewareDelegate _next; + private readonly ISystemClock _systemClock; + private readonly ILogger _logger; + + /// + /// Constructor. + /// + public ExceptionHandlingMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock, ILogger logger) + { + _next = next; + _systemClock = systemClock; + _logger = logger; + } + + /// + public async ValueTask InvokeAsync(WorkflowExecutionContext context) + { + try + { + await _next(context); + } + catch (Exception e) + { + _logger.LogWarning(e, "An exception was caught from a downstream middleware component"); + var exceptionState = ExceptionState.FromException(e); + var now = _systemClock.UtcNow; + var activity = context.Workflow; + var incident = new ActivityIncident(activity.Id, activity.Type, e.Message, exceptionState, now); + context.Incidents.Add(incident); + context.TransitionTo(WorkflowSubStatus.Faulted); + context.AddExecutionLogEntry("Faulted", e.Message, exceptionState); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs deleted file mode 100644 index b50c2c996..000000000 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Elsa.Workflows.Core.Models; - -/// -/// Holds information about a workflow fault. -/// -/// The exception that occurred -/// A description about the fault. Usually the exception message, if there waa an exception. -/// The ID of the activity that caused the workflow to fault. -public record WorkflowFault(Exception? Exception, string Message, string? FaultedActivityId); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs index b4cdce239..d09bd772d 100644 --- a/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs +++ b/src/modules/Elsa.Workflows.Core/Pipelines/WorkflowExecution/WorkflowExecutionPipeline.cs @@ -33,5 +33,7 @@ public class WorkflowExecutionPipeline : IWorkflowExecutionPipeline /// public async Task ExecuteAsync(WorkflowExecutionContext context) => await Pipeline(context); - private WorkflowMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x.UseDefaultActivityScheduler()); + private WorkflowMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x + .UseExceptionHandling() + .UseDefaultActivityScheduler()); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs index 2d828668d..f10418638 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs @@ -21,6 +21,7 @@ public static class WorkflowExecutionPipelineBuilderExtensions .UseBookmarkPersistence() .UseActivityExecutionLogPersistence() .UseWorkflowExecutionLogPersistence() + .UseExceptionHandling() .UseDefaultActivityScheduler(); ///