Implement Fault model

This commit is contained in:
Sipke Schoorstra 2022-12-26 18:37:29 +01:00
parent d7b41de7d3
commit 65e7009e56
13 changed files with 112 additions and 101 deletions

View file

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

View file

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

View file

@ -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;
/// <summary>
/// Faults the workflow.
/// </summary>
[Activity("Elsa", "Primitives", "Faults the workflow.")]
public class Fault : ActivityBase
{
/// <inheritdoc />
[JsonConstructor]
public Fault([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
{
}
/// <summary>
/// The message to include with the fault.
/// </summary>
[Input(Description = "The message to include with the fault.")]
public Input<string?> Message { get; set; } = default!;
/// <inheritdoc />
protected override void Execute(ActivityExecutionContext context)
{
var message = Message.Get(context);
throw new Exception(message);
}
}

View file

@ -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<string, object> GetOutputFrom(ActivityNode activityNode) =>
activityNode.GetType().GetProperties(BindingFlags.Public).Where(x => x.GetCustomAttribute<OutputAttribute>() != null).ToDictionary(x => x.Name, x => x.GetValue(activityNode)!);
var exceptionState = ExceptionState.FromException(fault.Exception);
return new WorkflowFaultState(exceptionState, fault.Message, fault.FaultedActivityId);
}
}

View file

@ -137,6 +137,11 @@ public class WorkflowExecutionContext
/// </summary>
public IDictionary<object, object> TransientProperties { get; set; } = new Dictionary<object, object>();
/// <summary>
/// Stores any fault that may have occurred during execution. Faulting a workflow will effectively suspend subsequent execution.
/// </summary>
public WorkflowFault? Fault { get; set; }
/// <summary>
/// The current <see cref="ExecuteActivityDelegate"/> delegate to invoke when executing the next activity.
/// </summary>

View file

@ -0,0 +1,10 @@
namespace Elsa.Workflows.Core.Models
{
/// <summary>
/// Holds information about a workflow fault.
/// </summary>
/// <param name="Exception">The exception that occurred</param>
/// <param name="Message">A description about the fault. Usually the exception message, if there waa an exception.</param>
/// <param name="FaultedActivityId">The ID of the activity that caused the workflow to fault.</param>
public record WorkflowFault(Exception? Exception, string Message, string? FaultedActivityId);
}

View file

@ -0,0 +1,32 @@
using System.Collections;
using System.Text.Json.Serialization;
// ReSharper disable NotAccessedPositionalProperty.Global
namespace Elsa.Workflows.Core.State
{
/// <summary>
/// A simplified, serializable model representing an exception.
/// </summary>
public record ExceptionState(Type Type, string Message, string? StackTrace, IDictionary Data, ExceptionState? InnerException = default)
{
// /// <summary>
// /// Constructor
// /// </summary>
// [JsonConstructor]
// public ExceptionState() : this(default!, default!, default, default!, default)
// {
//
// }
//
/// <summary>
/// Creates a new <see cref="ExceptionState"/> from the specified exception.
/// </summary>
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));
}
}
}

View file

@ -0,0 +1,10 @@
namespace Elsa.Workflows.Core.State
{
/// <summary>
/// Holds information about a workflow fault.
/// </summary>
/// <param name="Exception">The exception that occurred</param>
/// <param name="Message">A description about the fault. Usually the exception message, if there waa an exception.</param>
/// <param name="FaultedActivityId">The ID of the activity that caused the workflow to fault.</param>
public record WorkflowFaultState(ExceptionState? Exception, string Message, string? FaultedActivityId);
}

View file

@ -43,12 +43,12 @@ public class WorkflowState
/// Collected bookmarks.
/// </summary>
public ICollection<Bookmark> Bookmarks { get; set; } = new List<Bookmark>();
/// <summary>
/// A dictionary of activity output.
/// The serialized workflow state, if any.
/// </summary>
public IDictionary<string, IDictionary<string, object>> ActivityOutput { get; set; } = new Dictionary<string, IDictionary<string, object>>();
public WorkflowFaultState? Fault { get; set; }
/// <summary>
/// A list of callbacks that activities registered in order to be notified when the activities they scheduled complete.
/// </summary>

View file

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

View file

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

View file

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

View file

@ -1,4 +0,0 @@
namespace Elsa.Workflows.Management.Models
{
public record WorkflowFault(SimpleException? Exception, string Message, string? FaultedActivityId, object? ActivityInput, bool Resuming);
}