diff --git a/Directory.Build.props b/Directory.Build.props index 570aa11e8..cb6d30d9d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -33,6 +33,6 @@ $(NoWarn);CS0162;CS1591 - 3.2.0-rc3 + 3.3.0-preview.470 \ No newline at end of file diff --git a/README.md b/README.md index 86ff70daf..fc4e6eb7b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ELSA 3.0 -![Elsa Workflows](./design/artwork/elsa-logo-art.png) +![Elsa Workflows](./design/artwork/elsa-v3-avatar.png) [![Elsa 3 Prerelease](https://github.com/elsa-workflows/elsa-core/actions/workflows/packages.yml/badge.svg)](https://github.com/elsa-workflows/elsa-core/actions/workflows/packages.yml) [![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/Elsa)](https://www.nuget.org/packages/Elsa/) diff --git a/design/artwork/elsa-v3-avatar.png b/design/artwork/elsa-v3-avatar.png new file mode 100644 index 000000000..d87a1daa0 Binary files /dev/null and b/design/artwork/elsa-v3-avatar.png differ diff --git a/samples/console/Elsa.Samples.ConsoleApp.Bookmarks/Program.cs b/samples/console/Elsa.Samples.ConsoleApp.Bookmarks/Program.cs index adf1b64a2..4c5f20584 100644 --- a/samples/console/Elsa.Samples.ConsoleApp.Bookmarks/Program.cs +++ b/samples/console/Elsa.Samples.ConsoleApp.Bookmarks/Program.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection(); // Add Elsa services. -services.AddElsa(); +services.AddElsa().AddActivity(); // Build service container. var serviceProvider = services.BuildServiceProvider(); diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index 91868c131..7abd8ef97 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -16,18 +16,14 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows; -/// /// A delegate entry that is used by activities to be notified when the activities they scheduled are completed. -/// /// The activity scheduling the activity. /// The child being scheduled. /// The delegate to invoke when the scheduled activity completes. /// An optional tag. public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = default); -/// /// Provides context to the currently executing workflow. -/// [PublicAPI] public partial class WorkflowExecutionContext : IExecutionContext { @@ -39,9 +35,7 @@ public partial class WorkflowExecutionContext : IExecutionContext private IList _activityExecutionContexts; private readonly IHasher _hasher; - /// /// Initializes a new instance of . - /// private WorkflowExecutionContext( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -84,9 +78,7 @@ public partial class WorkflowExecutionContext : IExecutionContext _cancellationRegistrations.Add(linkedCancellationTokenSource.Token.Register(CancelWorkflow)); } - /// /// Creates a new for the specified workflow. - /// public static async Task CreateAsync( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -118,9 +110,7 @@ public partial class WorkflowExecutionContext : IExecutionContext ); } - /// /// Creates a new for the specified workflow. - /// public static async Task CreateAsync( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -154,9 +144,7 @@ public partial class WorkflowExecutionContext : IExecutionContext return workflowExecutionContext; } - /// /// Creates a new for the specified workflow. - /// public static async Task CreateAsync( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -197,9 +185,7 @@ public partial class WorkflowExecutionContext : IExecutionContext return workflowExecutionContext; } - /// /// Assigns the specified workflow to this workflow execution context. - /// /// The workflow graph to assign. public async Task SetWorkflowGraphAsync(WorkflowGraph workflowGraph) { @@ -215,257 +201,164 @@ public partial class WorkflowExecutionContext : IExecutionContext activityExecutionContext.Activity = workflowGraph.NodeIdLookup[activityExecutionContext.Activity.NodeId].Activity; } - /// /// Gets the . - /// public IServiceProvider ServiceProvider { get; } - /// /// Gets the . - /// public IActivityRegistry ActivityRegistry { get; } - /// /// Gets the . - /// public IActivityRegistryLookupService ActivityRegistryLookup { get; } - /// /// Gets the workflow graph. - /// public WorkflowGraph WorkflowGraph { get; private set; } - /// /// The associated with the execution context. - /// public Workflow Workflow => WorkflowGraph.Workflow; - /// /// A graph of the workflow structure. - /// public ActivityNode Graph => WorkflowGraph.Root; - /// - /// The current status of the workflow. - /// + /// The current status of the workflow. public WorkflowStatus Status => GetMainStatus(SubStatus); - /// /// The current sub status of the workflow. - /// public WorkflowSubStatus SubStatus { get; internal set; } - /// /// The root associated with the execution context. - /// public MemoryRegister MemoryRegister { get; private set; } = default!; - /// /// A unique ID of the execution context. - /// public string Id { get; set; } /// public IActivity Activity => Workflow; - /// /// An application-specific identifier associated with the execution context. - /// public string? CorrelationId { get; set; } - /// /// The ID of the workflow instance that triggered this instance. - /// public string? ParentWorkflowInstanceId { get; set; } - /// /// The date and time the workflow execution context was created. - /// public DateTimeOffset CreatedAt { get; set; } - /// /// The date and time the workflow execution context was last updated. - /// public DateTimeOffset UpdatedAt { get; set; } - /// /// The date and time the workflow execution context has finished. - /// public DateTimeOffset? FinishedAt { get; set; } - /// /// Gets the clock used to determine the current time. - /// public ISystemClock SystemClock { get; } - /// /// A flattened list of s from the . - /// public IReadOnlyCollection Nodes => WorkflowGraph.Nodes.ToList(); - /// /// A map between activity IDs and s in the workflow graph. - /// public IDictionary NodeIdLookup => WorkflowGraph.NodeIdLookup; - /// /// A map between hashed activity node IDs and s in the workflow graph. - /// public IDictionary NodeHashLookup => WorkflowGraph.NodeHashLookup; - /// /// A map between s and s in the workflow graph. - /// public IDictionary NodeActivityLookup => WorkflowGraph.NodeActivityLookup; - /// /// The for the execution context. - /// public IActivityScheduler Scheduler { get; } - /// /// Gets the . - /// public IIdentityGenerator IdentityGenerator { get; } - /// /// Gets the collection of original bookmarks associated with the workflow execution context. - /// public ICollection OriginalBookmarks { get; set; } - /// /// A collection of collected bookmarks during workflow execution. - /// public ICollection Bookmarks { get; set; } = new List(); - /// /// A diff between the original bookmarks and the current bookmarks. - /// public Diff BookmarksDiff => Diff.For(OriginalBookmarks, Bookmarks); /// /// A dictionary of inputs provided at the start of the current workflow execution. - /// public IDictionary Input { get; set; } - /// /// A dictionary of outputs provided by the current workflow execution. - /// public IDictionary Output { get; set; } = new Dictionary(); /// public IDictionary Properties { get; set; } - /// /// A dictionary that can be used by application code and middleware to store information and even services. Values do not need to be serializable. /// All data will be gone once workflow execution completes. - /// public IDictionary TransientProperties { get; set; } = new Dictionary(); - /// /// A collection of incidents that may have occurred during execution. - /// public ICollection Incidents { get; set; } - /// /// The current delegate to invoke when executing the next activity. - /// public ExecuteActivityDelegate? ExecuteDelegate { get; set; } - /// /// Provides context about the bookmark that was used to resume workflow execution, if any. - /// public ResumedBookmarkContext? ResumedBookmarkContext { get; set; } - /// /// The ID of the activity associated with the trigger that caused this workflow execution, if any. - /// public string? TriggerActivityId { get; set; } - /// /// A set of cancellation tokens that can be used to cancel the workflow execution without cancelling system-level operations. - /// public CancellationToken CancellationToken { get; } - /// /// A list of callbacks that are invoked when the associated child activity completes. - /// public ICollection CompletionCallbacks => new ReadOnlyCollection(_completionCallbackEntries); - /// /// A list of s that are currently active. - /// public IReadOnlyCollection ActivityExecutionContexts { get => _activityExecutionContexts.ToList(); internal set => _activityExecutionContexts = value.ToList(); } - /// /// The last execution log sequence number. This number is incremented every time a new entry is added to the execution log and is persisted alongside the workflow instance and restored when the workflow is resumed. - /// public long ExecutionLogSequence { get; set; } - /// /// A collection of execution log entries. This collection is flushed when the workflow execution context ends. - /// public ICollection ExecutionLog { get; } = new List(); - /// /// The expression execution context for the current workflow execution. - /// public ExpressionExecutionContext? ExpressionExecutionContext { get; private set; } /// public IEnumerable Variables => Workflow.Variables; - /// /// Resolves the specified service type from the service provider. - /// public T GetRequiredService() where T : notnull => ServiceProvider.GetRequiredService(); - /// /// Resolves the specified service type from the service provider. - /// public object GetRequiredService(Type serviceType) => ServiceProvider.GetRequiredService(serviceType); - /// /// Resolves the specified service type from the service provider, or creates a new instance if the service type was not found in the service container. - /// public T GetOrCreateService() where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance(ServiceProvider); - /// /// Resolves the specified service type from the service provider, or creates a new instance if the service type was not found in the service container. - /// public object GetOrCreateService(Type serviceType) => ActivatorUtilities.GetServiceOrCreateInstance(ServiceProvider, serviceType); - /// /// Resolves the specified service type from the service provider. - /// public T? GetService() where T : notnull => ServiceProvider.GetService(); - /// /// Resolves the specified service type from the service provider. - /// public object? GetService(Type serviceType) => ServiceProvider.GetService(serviceType); - /// /// Resolves multiple implementations of the specified service type from the service provider. - /// public IEnumerable GetServices() where T : notnull => ServiceProvider.GetServices(); - /// /// Registers a completion callback for the specified activity. - /// internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = default, object? tag = default) { var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback, tag); _completionCallbackEntries.Add(entry); } - /// /// Unregisters the completion callback for the specified owner and child activity. - /// internal ActivityCompletionCallbackEntry? PopCompletionCallback(ActivityExecutionContext owner, ActivityNode child) { var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child); @@ -505,64 +398,43 @@ public partial class WorkflowExecutionContext : IExecutionContext /// /// Returns the with the specified activity ID from the workflow graph. - /// public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : default; - /// /// 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 : default; - /// /// Returns the containing the specified activity from the workflow graph. - /// public ActivityNode? FindNodeByActivity(IActivity activity) { return NodeActivityLookup.TryGetValue(activity, out var node) ? node : default; } - /// /// 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; - /// /// Returns the with the specified activity instance ID. - /// public IActivity? FindActivityByInstanceId(string activityInstanceId) => ActivityExecutionContexts.FirstOrDefault(x => x.Id == activityInstanceId)?.Activity; - /// /// Returns a custom property with the specified key from the dictionary. - /// public T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? value.ConvertTo() : default; - /// /// Sets a custom property with the specified key on the dictionary. - /// public void SetProperty(string key, T value) => Properties[key] = value!; - /// /// Updates a custom property with the specified key on the dictionary. - /// public T UpdateProperty(string key, Func updater) { var value = GetProperty(key); @@ -571,16 +443,14 @@ public partial class WorkflowExecutionContext : IExecutionContext return value; } - /// /// Returns true if the dictionary contains the specified key. - /// public bool HasProperty(string name) => Properties.ContainsKey(name); - internal bool CanTransitionTo(WorkflowSubStatus targetSubStatus) => ValidateStatusTransition(targetSubStatus); + internal bool CanTransitionTo(WorkflowSubStatus targetSubStatus) => ValidateStatusTransition(); internal void TransitionTo(WorkflowSubStatus subStatus) { - if (!ValidateStatusTransition(SubStatus)) + if (!ValidateStatusTransition(subStatus)) throw new Exception($"Cannot transition from {SubStatus} to {subStatus}"); SubStatus = subStatus; @@ -596,9 +466,7 @@ public partial class WorkflowExecutionContext : IExecutionContext } } - /// /// Creates a new for the specified activity. - /// public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = default) { var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type); @@ -636,35 +504,23 @@ public partial class WorkflowExecutionContext : IExecutionContext return activityExecutionContext; } - /// /// Returns a register of recorded activity output. - /// public ActivityOutputRegister GetActivityOutputRegister() => TransientProperties.GetOrAdd(ActivityOutputRegistryKey, () => new ActivityOutputRegister()); - /// /// Returns the last activity result. - /// public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : default; - /// /// Adds the specified to the workflow execution context. - /// public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context); - /// /// Removes the specified from the workflow execution context. - /// public void RemoveActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Remove(context); - /// /// Removes the specified from the workflow execution context. - /// /// The predicate used to filter the activity execution contexts to remove. public void RemoveActivityExecutionContext(Func predicate) => _activityExecutionContexts.RemoveWhere(predicate); - /// /// Records the output of the specified activity into the current workflow execution context. - /// /// The of the activity. /// The name of the output. /// The value of the output. @@ -689,8 +545,9 @@ public partial class WorkflowExecutionContext : IExecutionContext WorkflowSubStatus.Suspended => WorkflowStatus.Running, _ => throw new ArgumentOutOfRangeException(nameof(subStatus), subStatus, null) }; - - private bool ValidateStatusTransition(WorkflowSubStatus targetSubStatus) + + // TODO: Check if we should not use the target subStatus here instead. + private bool ValidateStatusTransition() { var currentMainStatus = GetMainStatus(SubStatus); return currentMainStatus != WorkflowStatus.Finished; diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/EngineExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/EngineExceptionHandlingMiddleware.cs new file mode 100644 index 000000000..0987b567c --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/EngineExceptionHandlingMiddleware.cs @@ -0,0 +1,41 @@ +using Elsa.Common.Contracts; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Models; +using Elsa.Workflows.Pipelines.WorkflowExecution; +using Elsa.Workflows.State; +using Microsoft.Extensions.Logging; + +namespace Elsa.Workflows.Middleware.Workflows; + +/// Adds extension methods to . +public static class EngineExceptionHandlingMiddlewareExtensions +{ + /// Installs the component in the activity execution pipeline. + public static IWorkflowExecutionPipelineBuilder UseEngineExceptionHandling(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); +} + +/// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state. +public class EngineExceptionHandlingMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock, ILogger logger) : IWorkflowExecutionMiddleware +{ + /// + 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); + + // No state change as the workflow / activities status should be leading. + // We will however be adding an incident to make the issue visible. + context.Incidents.Add(incident); + context.AddExecutionLogEntry("Faulted", e.Message, exceptionState); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings index c67284c75..05e757dea 100644 --- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings @@ -1,4 +1,4 @@ - + True True True diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs index f65fcf45e..976948493 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs @@ -13,6 +13,8 @@ public static class WorkflowExecutionPipelineBuilderExtensions public static IWorkflowExecutionPipelineBuilder UseDefaultPipeline(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder .Reset() + .UseEngineExceptionHandling() + .UseBackgroundActivities() .UseBookmarkPersistence() .UseActivityExecutionLogPersistence() .UseWorkflowExecutionLogPersistence()