diff --git a/src/modules/Elsa.Persistence.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs b/src/modules/Elsa.Persistence.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs index f488673f0..36ca1b8ea 100644 --- a/src/modules/Elsa.Persistence.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs +++ b/src/modules/Elsa.Persistence.EntityFrameworkCore/Modules/Management/WorkflowInstanceStore.cs @@ -4,6 +4,7 @@ using Elsa.Common.Entities; using Elsa.Common.Models; using Elsa.Persistence.EntityFrameworkCore.Common; using Elsa.Persistence.EntityFrameworkCore.Extensions; +using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Serialization; using Elsa.Workflows.Core.State; using Elsa.Workflows.Management.Entities; @@ -130,13 +131,13 @@ public class EFCoreWorkflowInstanceStore : IWorkflowInstanceStore { } - public WorkflowInstanceState(WorkflowState workflowState, WorkflowFault? fault) + public WorkflowInstanceState(WorkflowState workflowState, WorkflowFaultState? fault) { WorkflowState = workflowState; Fault = fault; } public WorkflowState WorkflowState { get; init; } = default!; - public WorkflowFault? Fault { get; set; } + public WorkflowFaultState? Fault { get; set; } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs index a557411aa..756906550 100644 --- a/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs +++ b/src/modules/Elsa.Workflows.Api/Endpoints/WorkflowInstances/Get/Models.cs @@ -19,7 +19,7 @@ public class Response public WorkflowSubStatus SubStatus { get; set; } public string? CorrelationId { get; set; } public string? Name { get; set; } - public WorkflowFault? Fault { get; set; } + public WorkflowFaultState? Fault { get; set; } public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset? LastExecutedAt { get; set; } public DateTimeOffset? FinishedAt { get; set; } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Fault.cs b/src/modules/Elsa.Workflows.Core/Activities/Fault.cs new file mode 100644 index 000000000..f695a4ec8 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Activities/Fault.cs @@ -0,0 +1,32 @@ +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using Elsa.Workflows.Core.Attributes; +using Elsa.Workflows.Core.Models; + +namespace Elsa.Workflows.Core.Activities; + +/// +/// Faults the workflow. +/// +[Activity("Elsa", "Primitives", "Faults the workflow.")] +public class Fault : ActivityBase +{ + /// + [JsonConstructor] + public Fault([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The message to include with the fault. + /// + [Input(Description = "The message to include with the fault.")] + public Input Message { get; set; } = default!; + + /// + protected override void Execute(ActivityExecutionContext context) + { + var message = Message.Get(context); + throw new Exception(message); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs index 55aa98896..5792b7ff1 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs @@ -20,7 +20,8 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer CorrelationId = workflowExecutionContext.CorrelationId, Status = workflowExecutionContext.Status, SubStatus = workflowExecutionContext.SubStatus, - Bookmarks = workflowExecutionContext.Bookmarks + Bookmarks = workflowExecutionContext.Bookmarks, + Fault = SerializeFault(workflowExecutionContext.Fault) }; SerializeProperties(state, workflowExecutionContext); @@ -52,39 +53,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer workflowExecutionContext.Properties = state.Properties.Properties; } - private void GetOutput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) - { - foreach (var node in workflowExecutionContext.Nodes) - GetOutput(state, node); - } - - private void GetOutput(WorkflowState state, ActivityNode activityNode) - { - var output = GetOutputFrom(activityNode); - - if (output.Any()) - state.ActivityOutput.Add(activityNode.NodeId, output); - } - - private void SetOutput(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) - { - foreach (var entry in state.ActivityOutput) - { - var activityId = entry.Key; - var node = workflowExecutionContext.FindNodeById(activityId); - var activityType = node.Activity.GetType(); - - foreach (var outputEntry in entry.Value) - { - var propertyName = outputEntry.Key; - var propertyValue = outputEntry.Value; - var propertyInfo = activityType.GetProperty(propertyName, BindingFlags.Public)!; - propertyInfo.SetValue(node, propertyValue); - } - } - } - - private void DeserializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + private static void DeserializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { foreach (var completionCallbackEntry in state.CompletionCallbacks) { @@ -96,7 +65,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer } } - private void SerializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + private static void SerializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { // Assert all referenced owner contexts exist. foreach (var completionCallback in workflowExecutionContext.CompletionCallbacks) @@ -111,7 +80,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer state.CompletionCallbacks = completionCallbacks.ToList(); } - private void SerializeActivityExecutionContexts(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + private static void SerializeActivityExecutionContexts(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { ActivityExecutionContextState CreateActivityExecutionContextState(ActivityExecutionContext activityExecutionContext) { @@ -139,7 +108,7 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer state.ActivityExecutionContexts = workflowExecutionContext.ActivityExecutionContexts.Reverse().Select(CreateActivityExecutionContextState).ToList(); } - private void DeserializeActivityExecutionContexts(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) + private static void DeserializeActivityExecutionContexts(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { ActivityExecutionContext CreateActivityExecutionContext(ActivityExecutionContextState activityExecutionContextState) { @@ -167,7 +136,13 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer workflowExecutionContext.ActivityExecutionContexts = activityExecutionContexts; } + + private static WorkflowFaultState? SerializeFault(WorkflowFault? fault) + { + if (fault == null) + return null; - private Dictionary GetOutputFrom(ActivityNode activityNode) => - activityNode.GetType().GetProperties(BindingFlags.Public).Where(x => x.GetCustomAttribute() != null).ToDictionary(x => x.Name, x => x.GetValue(activityNode)!); + var exceptionState = ExceptionState.FromException(fault.Exception); + return new WorkflowFaultState(exceptionState, fault.Message, fault.FaultedActivityId); + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs index b61277ec3..d4e364031 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs @@ -137,6 +137,11 @@ public class WorkflowExecutionContext /// public IDictionary TransientProperties { get; set; } = new Dictionary(); + /// + /// Stores any fault that may have occurred during execution. Faulting a workflow will effectively suspend subsequent execution. + /// + public WorkflowFault? Fault { get; set; } + /// /// The current delegate to invoke when executing the next activity. /// diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs new file mode 100644 index 000000000..448a041ea --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowFaultState.cs @@ -0,0 +1,10 @@ +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/State/ExceptionState.cs b/src/modules/Elsa.Workflows.Core/State/ExceptionState.cs new file mode 100644 index 000000000..33075a417 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/State/ExceptionState.cs @@ -0,0 +1,32 @@ +using System.Collections; +using System.Text.Json.Serialization; +// ReSharper disable NotAccessedPositionalProperty.Global + +namespace Elsa.Workflows.Core.State +{ + /// + /// A simplified, serializable model representing an exception. + /// + public record ExceptionState(Type Type, string Message, string? StackTrace, IDictionary Data, ExceptionState? InnerException = default) + { + // /// + // /// Constructor + // /// + // [JsonConstructor] + // public ExceptionState() : this(default!, default!, default, default!, default) + // { + // + // } + // + /// + /// Creates a new from the specified exception. + /// + public static ExceptionState? FromException(Exception? ex) + { + if (ex == null) + return null; + + return new ExceptionState(ex.GetType(), ex.Message, ex.StackTrace, ex.Data, FromException(ex.InnerException)); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/State/WorkflowFaultState.cs b/src/modules/Elsa.Workflows.Core/State/WorkflowFaultState.cs new file mode 100644 index 000000000..350ca663f --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/State/WorkflowFaultState.cs @@ -0,0 +1,10 @@ +namespace Elsa.Workflows.Core.State +{ + /// + /// 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 WorkflowFaultState(ExceptionState? Exception, string Message, string? FaultedActivityId); +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs b/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs index b0c297be6..a8b7d8d9d 100644 --- a/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs +++ b/src/modules/Elsa.Workflows.Core/State/WorkflowState.cs @@ -43,12 +43,12 @@ public class WorkflowState /// Collected bookmarks. /// public ICollection Bookmarks { get; set; } = new List(); - + /// - /// A dictionary of activity output. + /// The serialized workflow state, if any. /// - public IDictionary> ActivityOutput { get; set; } = new Dictionary>(); - + public WorkflowFaultState? Fault { get; set; } + /// /// A list of callbacks that activities registered in order to be notified when the activities they scheduled complete. /// diff --git a/src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs b/src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs index 7684d4da1..bc55def88 100644 --- a/src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs +++ b/src/modules/Elsa.Workflows.Management/Entities/WorkflowInstance.cs @@ -16,7 +16,7 @@ public class WorkflowInstance : Entity public WorkflowSubStatus SubStatus { get; set; } public string? CorrelationId { get; set; } public string? Name { get; set; } - public WorkflowFault? Fault { get; set; } + public WorkflowFaultState? Fault { get; set; } public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset? LastExecutedAt { get; set; } public DateTimeOffset? FinishedAt { get; set; } diff --git a/src/modules/Elsa.Workflows.Management/Models/SimpleException.cs b/src/modules/Elsa.Workflows.Management/Models/SimpleException.cs deleted file mode 100644 index ec3d050c2..000000000 --- a/src/modules/Elsa.Workflows.Management/Models/SimpleException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Collections; - -namespace Elsa.Workflows.Management.Models -{ - public class SimpleException - { - public SimpleException(Type type, string message, string? stackTrace, IDictionary data, SimpleException? innerException = default) - { - Type = type; - Message = message; - StackTrace = stackTrace; - InnerException = innerException; - Data = data; - } - - public Type Type { get; set; } - public string Message { get; set; } - public string? StackTrace { get; set; } - public SimpleException? InnerException { get; set; } - public IDictionary Data { get; set; } - - public static SimpleException? FromException(Exception? ex) - { - if (ex == null) - return null; - - var exceptionType = ex.GetType(); - var simpleException = new SimpleException(exceptionType, ex.Message, ex.StackTrace, ex.Data); - - if (ex.InnerException != null) - simpleException.InnerException = FromException(ex.InnerException); - - return simpleException; - } - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Models/SimpleExceptionProperty.cs b/src/modules/Elsa.Workflows.Management/Models/SimpleExceptionProperty.cs deleted file mode 100644 index 62feba6ca..000000000 --- a/src/modules/Elsa.Workflows.Management/Models/SimpleExceptionProperty.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Elsa.Workflows.Management.Models -{ - public class SimpleExceptionProperty - { - public SimpleExceptionProperty(string name, object value) - { - Name = name; - Value = value; - } - - public string Name { get; set; } - public object Value { get; set; } - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Models/WorkflowFault.cs b/src/modules/Elsa.Workflows.Management/Models/WorkflowFault.cs deleted file mode 100644 index a62d846f2..000000000 --- a/src/modules/Elsa.Workflows.Management/Models/WorkflowFault.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Elsa.Workflows.Management.Models -{ - public record WorkflowFault(SimpleException? Exception, string Message, string? FaultedActivityId, object? ActivityInput, bool Resuming); -} \ No newline at end of file