diff --git a/src/modules/Elsa.ActivityDefinitions/Activities/ActivityDefinitionActivity.cs b/src/modules/Elsa.ActivityDefinitions/Activities/ActivityDefinitionActivity.cs index bc6ec790f..2819d2e13 100644 --- a/src/modules/Elsa.ActivityDefinitions/Activities/ActivityDefinitionActivity.cs +++ b/src/modules/Elsa.ActivityDefinitions/Activities/ActivityDefinitionActivity.cs @@ -10,6 +10,7 @@ namespace Elsa.ActivityDefinitions.Activities; /// public class ActivityDefinitionActivity : ActivityBase { + /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // Construct the root activity stored in the activity definitions. @@ -17,8 +18,8 @@ public class ActivityDefinitionActivity : ActivityBase var root = await materializer.MaterializeAsync(this, context.CancellationToken); // Schedule the activity for execution. - await context.ScheduleActivityAsync(root, onChildCompletedAsync); + await context.ScheduleActivityAsync(root, OnChildCompletedAsync); } - private async ValueTask onChildCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); + private async ValueTask OnChildCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); } \ No newline at end of file diff --git a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs index e2faa861e..5f3f70e50 100644 --- a/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs +++ b/src/modules/Elsa.Expressions/Helpers/ObjectConverter.cs @@ -32,7 +32,7 @@ public static class ObjectConverter if (value is DahomeyJsonNode { ValueKind: JsonValueKind.Object } dahomyJsonObject) return ToObject(dahomyJsonObject, targetType, options); - if (value is JsonElement { ValueKind: JsonValueKind.Object } jsonObject) + if (value is JsonElement { ValueKind: JsonValueKind.Object or JsonValueKind.Array } jsonObject) return jsonObject.Deserialize(targetType, options); var underlyingTargetType = Nullable.GetUnderlyingType(targetType) ?? targetType; diff --git a/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs b/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs index 60a73d448..655b96361 100644 --- a/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs +++ b/src/modules/Elsa.JavaScript/Providers/JavaScriptExpressionSyntaxProvider.cs @@ -27,15 +27,7 @@ public class JavaScriptExpressionSyntaxProvider : IExpressionSyntaxProvider Syntax = SyntaxName, Type = typeof(JavaScriptExpression), CreateExpression = CreateJavaScriptExpression, - CreateBlockReference = context => - { - var reference = new JavaScriptExpressionBlockReference(context.GetExpression()); - - if (string.IsNullOrWhiteSpace(reference.Id)) - reference.Id = GenerateId(); - - return reference; - }, + CreateBlockReference = context => new JavaScriptExpressionBlockReference(context.GetExpression()), CreateSerializableObject = context => new { Type = SyntaxName, diff --git a/src/modules/Elsa.Liquid/Providers/LiquidExpressionSyntaxProvider.cs b/src/modules/Elsa.Liquid/Providers/LiquidExpressionSyntaxProvider.cs index b4948670b..ee5b667f1 100644 --- a/src/modules/Elsa.Liquid/Providers/LiquidExpressionSyntaxProvider.cs +++ b/src/modules/Elsa.Liquid/Providers/LiquidExpressionSyntaxProvider.cs @@ -23,15 +23,7 @@ public class LiquidExpressionSyntaxProvider : IExpressionSyntaxProvider Syntax = SyntaxName, Type = typeof(LiquidExpression), CreateExpression = CreateLiquidExpression, - CreateBlockReference = context => - { - var reference = new LiquidExpressionBlockReference(context.GetExpression()); - - if (string.IsNullOrWhiteSpace(reference.Id)) - reference.Id = GenerateId(); - - return reference; - }, + CreateBlockReference = context => new LiquidExpressionBlockReference(context.GetExpression()), CreateSerializableObject = context => new { Type = SyntaxName, diff --git a/src/modules/Elsa.ProtoActor/Grains/WorkflowGrain.cs b/src/modules/Elsa.ProtoActor/Grains/WorkflowGrain.cs index 1800a7381..d467c0e89 100644 --- a/src/modules/Elsa.ProtoActor/Grains/WorkflowGrain.cs +++ b/src/modules/Elsa.ProtoActor/Grains/WorkflowGrain.cs @@ -176,6 +176,7 @@ public class WorkflowGrain : WorkflowGrainBase ActivityInstanceId = x.ActivityInstanceId, Hash = x.Hash, Data = x.Data.EmptyIfNull(), + AutoBurn = x.AutoBurn, CallbackMethodName = x.CallbackMethodName.EmptyIfNull() }); } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs b/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs index a38fd9f16..d780aad0d 100644 --- a/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs +++ b/src/modules/Elsa.ProtoActor/Implementations/ProtoActorWorkflowRuntime.cs @@ -244,5 +244,6 @@ public class ProtoActorWorkflowRuntime : IWorkflowRuntime x.Data.NullIfEmpty(), x.ActivityId, x.ActivityInstanceId, + x.AutoBurn, x.CallbackMethodName.NullIfEmpty())); } \ No newline at end of file diff --git a/src/modules/Elsa.ProtoActor/Protos/Messages.proto b/src/modules/Elsa.ProtoActor/Protos/Messages.proto index 1cb5379f2..013ee8bda 100644 --- a/src/modules/Elsa.ProtoActor/Protos/Messages.proto +++ b/src/modules/Elsa.ProtoActor/Protos/Messages.proto @@ -81,7 +81,8 @@ message BookmarkDto { optional string Data = 4; string ActivityId = 5; string ActivityInstanceId = 6; - optional string CallbackMethodName = 7; + optional bool AutoBurn = 7; + optional string CallbackMethodName = 8; } message Json { diff --git a/src/modules/Elsa.Scheduling/Activities/Delay.cs b/src/modules/Elsa.Scheduling/Activities/Delay.cs index f69734eb2..e6aaca98c 100644 --- a/src/modules/Elsa.Scheduling/Activities/Delay.cs +++ b/src/modules/Elsa.Scheduling/Activities/Delay.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; using Elsa.Common.Services; using Elsa.Expressions.Models; using Elsa.Scheduling.Models; @@ -8,42 +9,72 @@ using Elsa.Workflows.Core.Models; namespace Elsa.Scheduling.Activities; +/// +/// Delay execution for the specified amount of time. +/// [Activity( "Elsa", "Scheduling", "Delay execution for the specified amount of time.")] public class Delay : Activity { + /// [JsonConstructor] - public Delay() + public Delay([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public Delay(Func timeSpan, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) : this(new Input(timeSpan)) + /// + public Delay( + Func timeSpan, + DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking, + [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Input(timeSpan), blockingStrategy, source, line) { } - - public Delay(Func> timeSpan, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) : this(new Input(timeSpan)) + + /// + public Delay( + Func> timeSpan, + DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking, + [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Input(timeSpan), blockingStrategy, source, line) { } - - public Delay(Input timeSpan, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) + + /// + public Delay( + Input timeSpan, + DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking, + [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { TimeSpan = timeSpan; Strategy = blockingStrategy; } - public Delay(TimeSpan timeSpan, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) + /// + public Delay( + TimeSpan timeSpan, + DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking, + [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { TimeSpan = new Input(timeSpan); Strategy = blockingStrategy; } - public Delay(Variable timeSpan, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) + /// + public Delay( + Variable timeSpan, + DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking, + [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { TimeSpan = new Input(timeSpan); Strategy = blockingStrategy; } + /// + /// The amount of time to delay execution. + /// [Input] public Input TimeSpan { get; set; } = default!; + /// + /// A value controlling whether the delay should happen in-process (synchronously or out of process (asynchronously). + /// [Input] public DelayBlockingStrategy Strategy { get; set; } = DelayBlockingStrategy.NonBlocking; /// @@ -97,11 +128,33 @@ public class Delay : Activity await NonBlockingStrategy(timeSpan, context); } + /// + /// Creates a new from the specified number of milliseconds. + /// public static Delay FromMilliseconds(double value, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) => new(System.TimeSpan.FromMilliseconds(value), blockingStrategy); + + /// + /// Creates a new from the specified number of seconds. + /// public static Delay FromSeconds(double value, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) => new(System.TimeSpan.FromSeconds(value), blockingStrategy); + + /// + /// Creates a new from the specified number of minutes. + /// public static Delay FromMinutes(double value, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) => new(System.TimeSpan.FromMinutes(value), blockingStrategy); + + /// + /// Creates a new from the specified number of hours. + /// public static Delay FromHours(double value, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) => new(System.TimeSpan.FromHours(value), blockingStrategy); + + /// + /// Creates a new from the specified number of days. + /// public static Delay FromDays(double value, DelayBlockingStrategy blockingStrategy = DelayBlockingStrategy.NonBlocking) => new(System.TimeSpan.FromDays(value), blockingStrategy); } +/// +/// A bookmark payload for . +/// public record DelayPayload(DateTimeOffset ResumeAt); \ No newline at end of file diff --git a/src/modules/Elsa.Scheduling/Activities/StartAt.cs b/src/modules/Elsa.Scheduling/Activities/StartAt.cs index fd9c9c8af..ea06d2ae5 100644 --- a/src/modules/Elsa.Scheduling/Activities/StartAt.cs +++ b/src/modules/Elsa.Scheduling/Activities/StartAt.cs @@ -1,4 +1,6 @@ -using Elsa.Common.Services; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using Elsa.Common.Services; using Elsa.Expressions.Models; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; @@ -7,44 +9,69 @@ using Microsoft.Extensions.Logging; namespace Elsa.Scheduling.Activities; +/// +/// Triggers the workflow at a specific future timestamp. +/// [Activity("Elsa", "Scheduling", "Trigger execution at a specific time in the future.")] public class StartAt : Trigger { - public const string InputKey = "ExecuteAt"; - - public StartAt() + private const string InputKey = "ExecuteAt"; + + /// + [JsonConstructor] + public StartAt([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public StartAt(Input dateTime) => DateTime = dateTime; + /// + public StartAt(Input dateTime, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => DateTime = dateTime; - public StartAt(Func dateTime) : this(new Input(dateTime)) - { - } - - public StartAt(Func> dateTime) : this(new Input(dateTime)) - { - } - - public StartAt(Func> dateTime) : this(new Input(dateTime)) - { - } - - public StartAt(Func dateTime) : this(new Input(dateTime)) + /// + public StartAt(Func dateTime, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(dateTime), source, line) { } - public StartAt(DateTimeOffset dateTime) => DateTime = new Input(dateTime); - public StartAt(Variable dateTime) => DateTime = new Input(dateTime); + /// + public StartAt( + Func> dateTime, + [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Input(dateTime), source, line) + { + } + /// + public StartAt(Func> dateTime, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(dateTime), source, line) + { + } + + /// + public StartAt(Func dateTime, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(dateTime), source, line) + { + } + + /// + public StartAt(DateTimeOffset dateTime, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + DateTime = new Input(dateTime); + + /// + public StartAt(Variable dateTime, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + DateTime = new Input(dateTime); + + /// + /// The timestamp at which the workflow should be triggered. + /// [Input] public Input DateTime { get; set; } = default!; + /// protected override object GetTriggerPayload(TriggerIndexingContext context) { var executeAt = context.ExpressionExecutionContext.Get(DateTime); return new StartAtPayload(executeAt); } + /// protected override void Execute(ActivityExecutionContext context) { // If external input was received, it means this activity got triggered and does not need to create a bookmark. @@ -69,7 +96,10 @@ public class StartAt : Trigger context.CreateBookmark(payload); } + /// + /// Creates a new activity set to trigger at the specified timestamp. + /// public static StartAt From(DateTimeOffset value) => new(value); } -public record StartAtPayload(DateTimeOffset ExecuteAt); \ No newline at end of file +internal record StartAtPayload(DateTimeOffset ExecuteAt); \ No newline at end of file diff --git a/src/modules/Elsa.Scheduling/Activities/Timer.cs b/src/modules/Elsa.Scheduling/Activities/Timer.cs index 740dbeb1c..d3affe586 100644 --- a/src/modules/Elsa.Scheduling/Activities/Timer.cs +++ b/src/modules/Elsa.Scheduling/Activities/Timer.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; using Elsa.Common.Services; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; @@ -12,22 +13,29 @@ namespace Elsa.Scheduling.Activities; [Activity( "Elsa", "Scheduling", "Trigger workflow execution at a specific interval.")] public class Timer : EventGenerator { + /// [JsonConstructor] - public Timer() + public Timer([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public Timer(TimeSpan interval) : this(new Input(interval)) + /// + public Timer(TimeSpan interval, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Input(interval), source, line) { } - public Timer(Input interval) + /// + public Timer(Input interval, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) { Interval = interval; } + /// + /// Th interval at which the timer should execute. + /// [Input] public Input Interval { get; set; } = default!; + /// protected override object GetTriggerPayload(TriggerIndexingContext context) { var interval = context.ExpressionExecutionContext.Get(Interval); @@ -36,8 +44,15 @@ public class Timer : EventGenerator return new TimerPayload(executeAt, interval); } + /// + /// Creates a new activity set to trigger at the specified interval. + /// public static Timer FromTimeSpan(TimeSpan value) => new(value); + + /// + /// Creates a new activity set to trigger at the specified interval in seconds. + /// public static Timer FromSeconds(double value) => FromTimeSpan(TimeSpan.FromSeconds(value)); } -public record TimerPayload(DateTimeOffset StartAt, TimeSpan Interval); \ No newline at end of file +internal record TimerPayload(DateTimeOffset StartAt, TimeSpan Interval); \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/AnswerCall.cs b/src/modules/Elsa.Telnyx/Activities/AnswerCall.cs index 8313c72b5..5f542455c 100644 --- a/src/modules/Elsa.Telnyx/Activities/AnswerCall.cs +++ b/src/modules/Elsa.Telnyx/Activities/AnswerCall.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Attributes; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; @@ -9,19 +10,64 @@ using Elsa.Workflows.Core.Activities.Flowchart.Attributes; using Elsa.Workflows.Core.Activities.Flowchart.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Core.Services; using Elsa.Workflows.Runtime.Services; using Refit; namespace Elsa.Telnyx.Activities; +/// +[FlowNode("Connected", "Disconnected")] +public class FlowAnswerCall : AnswerCallBase +{ + /// + public FlowAnswerCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + protected override async ValueTask HandleConnectedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(new Outcomes("Connected")); + + /// + protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(new Outcomes("Disconnected")); +} + +/// +public class AnswerCall : AnswerCallBase +{ + /// + public AnswerCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The activity to schedule when the call was successfully answered. + /// + [Port] + public IActivity? Connected { get; set; } + + /// + /// The activity to schedule when the call was no longer active. + /// + [Port] + public IActivity? Disconnected { get; set; } + + protected override async ValueTask HandleConnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Connected); + protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected); +} + /// /// Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call. /// [Activity(Constants.Namespace, "Answer an incoming call. You must issue this command before executing subsequent commands on an incoming call.", Kind = ActivityKind.Task)] -[FlowNode("Connected", "Disconnected")] [WebhookDriven(WebhookEventTypes.CallAnswered)] -public class AnswerCall : ActivityBase, IBookmarksPersistedHandler +public abstract class AnswerCallBase : ActivityBase, IBookmarksPersistedHandler { + /// + protected AnswerCallBase(string? source = default, int? line = default) : base(source, line) + { + } + /// /// The call control ID to answer. Leave blank when the workflow is driven by an incoming call and you wish to pick up that one. /// @@ -38,14 +84,16 @@ public class AnswerCall : ActivityBase, IBookmarksPersisted /// /// Invokes Telnyx to answer the call. /// - /// public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context) => await InvokeTelnyxAsync(context); - + + protected abstract ValueTask HandleConnectedAsync(ActivityExecutionContext context); + protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context); + private async ValueTask ResumeAsync(ActivityExecutionContext context) { var payload = context.GetInput(); context.Set(Result, payload); - await context.CompleteActivityAsync(new Outcomes("Connected")); + await HandleConnectedAsync(context); } /// @@ -56,15 +104,15 @@ public class AnswerCall : ActivityBase, IBookmarksPersisted var callControlId = context.GetPrimaryCallControlId(CallControlId) ?? throw new Exception("CallControlId is required."); var request = new AnswerCallRequest(); var telnyxClient = context.GetRequiredService(); - + try { - await telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken); + await telnyxClient.Calls.AnswerCallAsync(callControlId, request, context.CancellationToken); } catch (ApiException e) { if (!await e.CallIsNoLongerActiveAsync()) throw; - await context.CompleteActivityAsync(new Outcomes("Disconnected")); + await HandleDisconnectedAsync(context); } } } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/BridgeCalls.cs b/src/modules/Elsa.Telnyx/Activities/BridgeCalls.cs index f7c777327..519a71e5b 100644 --- a/src/modules/Elsa.Telnyx/Activities/BridgeCalls.cs +++ b/src/modules/Elsa.Telnyx/Activities/BridgeCalls.cs @@ -1,30 +1,55 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; +using Elsa.Telnyx.Bookmarks; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Telnyx.Extensions; using Elsa.Telnyx.Payloads.Call; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Activities.Flowchart.Attributes; -using Elsa.Workflows.Core.Activities.Flowchart.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; +using Elsa.Workflows.Runtime.Services; using Refit; namespace Elsa.Telnyx.Activities; +/// [FlowNode("Bridged", "Disconnected")] public class FlowBridgeCalls : BridgeCallsBase { + /// + public FlowBridgeCalls([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityAsync("Disconnected"); protected override ValueTask HandleBridgedAsync(ActivityExecutionContext context) => context.CompleteActivityAsync("Bridged"); } +/// public class BridgeCalls : BridgeCallsBase { - [Port]public IActivity? Disconnected { get; set; } - [Port]public IActivity? Bridged { get; set; } + /// + public BridgeCalls([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// + /// The to execute when the source leg call is no longer active. + /// + [Port]public IActivity? Disconnected { get; set; } + + /// + /// The to execute when the two calls are bridged. + /// + [Port]public IActivity? Bridged { get; set; } + + /// protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompleted); + + /// protected override async ValueTask HandleBridgedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Bridged, OnCompleted); } @@ -32,9 +57,13 @@ public class BridgeCalls : BridgeCallsBase /// Bridge two calls. /// [Activity(Constants.Namespace, "Bridge two calls.", Kind = ActivityKind.Task)] -[FlowNode("Bridged", "Disconnected")] -public abstract class BridgeCallsBase : ActivityBase +public abstract class BridgeCallsBase : ActivityBase, IBookmarksPersistedHandler { + /// + protected BridgeCallsBase(string? source = default, int? line = default) : base(source, line) + { + } + /// /// The source call control ID of one of the call to bridge with. Leave empty to use the ambient inbound call control Id, if there is one. /// @@ -48,7 +77,7 @@ public abstract class BridgeCallsBase : ActivityBase public Input? CallControlIdB { get; set; } /// - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + public async ValueTask BookmarksPersistedAsync(ActivityExecutionContext context) { var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA) ?? throw new Exception("CallControlA is required"); var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB) ?? throw new Exception("CallControlB is required"); @@ -58,26 +87,34 @@ public abstract class BridgeCallsBase : ActivityBase try { await telnyxClient.Calls.BridgeCallsAsync(callControlIdA, request, context.CancellationToken); - context.CreateBookmark(ResumeAsync); } catch (ApiException e) { if (!await e.CallIsNoLongerActiveAsync()) throw; - await context.CompleteActivityAsync("Disconnected"); + await HandleDisconnectedAsync(context); } } + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) + { + var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA) ?? throw new Exception("CallControlA is required"); + var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB) ?? throw new Exception("CallControlB is required"); + var bookmarkA = new CallBridgedBookmarkPayload(callControlIdA); + var bookmarkB = new CallBridgedBookmarkPayload(callControlIdB); + context.CreateBookmarks(new[]{ bookmarkA, bookmarkB }, ResumeAsync); + } + protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context); protected abstract ValueTask HandleBridgedAsync(ActivityExecutionContext context); - protected async ValueTask OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); private async ValueTask ResumeAsync(ActivityExecutionContext context) { var payload = context.GetInput()!; var callControlIdA = context.GetPrimaryCallControlId(CallControlIdA); - var callControlIdB = context.GetPrimaryCallControlId(CallControlIdA); + var callControlIdB = context.GetSecondaryCallControlId(CallControlIdB); if (payload.CallControlId == callControlIdA) context.SetProperty("CallBridgedPayloadA", payload); if (payload.CallControlId == callControlIdB) context.SetProperty("CallBridgedPayloadB", payload); @@ -88,11 +125,8 @@ public abstract class BridgeCallsBase : ActivityBase if (callBridgedPayloadA != null && callBridgedPayloadB != null) { context.Set(Result, new BridgedCallsOutput(callBridgedPayloadA, callBridgedPayloadB)); - await context.CompleteActivityAsync(new Outcomes("Bridged")); - return; + await HandleBridgedAsync(context); } - - context.CreateBookmark(); } } diff --git a/src/modules/Elsa.Telnyx/Activities/Dial.cs b/src/modules/Elsa.Telnyx/Activities/Dial.cs index d02f0b9ac..2860648e2 100644 --- a/src/modules/Elsa.Telnyx/Activities/Dial.cs +++ b/src/modules/Elsa.Telnyx/Activities/Dial.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Attributes; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; @@ -23,6 +24,11 @@ namespace Elsa.Telnyx.Activities; [WebhookDriven(WebhookEventTypes.CallAnswered, WebhookEventTypes.CallHangup, WebhookEventTypes.CallMachineGreetingEnded, WebhookEventTypes.CallMachinePremiumGreetingEnded)] public abstract class DialBase : ActivityBase { + /// + protected DialBase(string? source = default, int? line = default) : base(source, line) + { + } + [Input(Description = "The DID or SIP URI to dial out and bridge to the given call.")] public Input To { get; set; } = default!; @@ -117,32 +123,57 @@ public abstract class DialBase : ActivityBase } } +/// [FlowNode("Answered", "Hangup", "Voicemail")] public class FlowDial : DialBase { + private FlowDial([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// protected override async ValueTask OnHandleAnsweredAsync(ActivityExecutionContext context, CallAnsweredPayload payload) => await context.CompleteActivityWithOutcomesAsync("Answered"); + + /// protected override async ValueTask OnHandleHangupAsync(ActivityExecutionContext context, CallHangupPayload payload) => await context.CompleteActivityWithOutcomesAsync("Hangup"); + + /// protected override async ValueTask OnHandleMachineGreetingEndedAsync(ActivityExecutionContext context, CallMachineGreetingEndedBase payload) => await context.CompleteActivityWithOutcomesAsync("Voicemail"); } +/// +/// Dial a phone number or SIP URI. +/// public class Dial : DialBase { + /// + public Dial([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The to execute when the call was answered. + /// [Port] public IActivity? Answered { get; set; } + + /// + /// The to execute when there is no reply. + /// [Port] public IActivity? Hangup { get; set; } + + /// + /// The to execute when a robot answered the call. + /// [Port] public IActivity? Voicemail { get; set; } - protected override async ValueTask OnHandleAnsweredAsync(ActivityExecutionContext context, CallAnsweredPayload payload) - { + /// + protected override async ValueTask OnHandleAnsweredAsync(ActivityExecutionContext context, CallAnsweredPayload payload) => await context.ScheduleActivityAsync(Answered); - } - protected override async ValueTask OnHandleHangupAsync(ActivityExecutionContext context, CallHangupPayload payload) - { - await context.ScheduleActivityAsync(Hangup); - } + /// + protected override async ValueTask OnHandleHangupAsync(ActivityExecutionContext context, CallHangupPayload payload) => await context.ScheduleActivityAsync(Hangup); - protected override async ValueTask OnHandleMachineGreetingEndedAsync(ActivityExecutionContext context, CallMachineGreetingEndedBase payload) - { + /// + protected override async ValueTask OnHandleMachineGreetingEndedAsync(ActivityExecutionContext context, CallMachineGreetingEndedBase payload) => await context.ScheduleActivityAsync(Voicemail); - } } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs b/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs index 7e562fe02..a691873a3 100644 --- a/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs +++ b/src/modules/Elsa.Telnyx/Activities/GatherUsingAudio.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Attributes; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; @@ -21,6 +22,11 @@ namespace Elsa.Telnyx.Activities; [WebhookDriven(WebhookEventTypes.CallGatherEnded)] public class GatherUsingAudio : ActivityBase, IBookmarksPersistedHandler { + /// + public GatherUsingAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// The call control ID of the call from which to gather input. Leave empty to use the ambient call control ID, if there is any. /// diff --git a/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs b/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs index 5060b00fc..c839a5996 100644 --- a/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs +++ b/src/modules/Elsa.Telnyx/Activities/GatherUsingSpeak.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Attributes; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; @@ -22,6 +23,11 @@ namespace Elsa.Telnyx.Activities; [WebhookDriven(WebhookEventTypes.CallGatherEnded)] public class GatherUsingSpeak : ActivityBase, IBookmarksPersistedHandler { + /// + public GatherUsingSpeak([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// The call control ID of the call from which to gather input. Leave empty to use the ambient call control ID, if there is any. /// diff --git a/src/modules/Elsa.Telnyx/Activities/HangupCall.cs b/src/modules/Elsa.Telnyx/Activities/HangupCall.cs index 0c9244af8..335a2dc4f 100644 --- a/src/modules/Elsa.Telnyx/Activities/HangupCall.cs +++ b/src/modules/Elsa.Telnyx/Activities/HangupCall.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Telnyx.Extensions; using Elsa.Workflows.Core; @@ -10,20 +11,45 @@ using Refit; namespace Elsa.Telnyx.Activities; +/// [FlowNode("Done", "Disconnected")] public class FlowHangupCall : HangupCallBase { + /// + public FlowHangupCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// protected override ValueTask HandleDoneAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Done"); + + /// protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected"); } +/// public class HangupCall : HangupCallBase { - [Port] public IActivity? Done { get; set; } + /// + public HangupCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The to execute when the call was no longer active. + /// [Port] public IActivity? Disconnected { get; set; } - protected override async ValueTask HandleDoneAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Done, OnCompletedAsync); + /// + protected override async ValueTask HandleDoneAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(OnCompletedAsync); + + /// protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync); + + /// + /// Executed when any child activity completed. + /// + private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); } /// @@ -32,6 +58,11 @@ public class HangupCall : HangupCallBase [Activity(Constants.Namespace, "Hang up the call.", Kind = ActivityKind.Task)] public abstract class HangupCallBase : ActivityBase { + /// + protected HangupCallBase([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// @@ -57,7 +88,13 @@ public abstract class HangupCallBase : ActivityBase } } + /// + /// Executed when the call was hangup. + /// protected abstract ValueTask HandleDoneAsync(ActivityExecutionContext context); + + /// + /// Executed when the call was no longer active. + /// protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context); - protected async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs b/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs index f422f787e..66ace6af3 100644 --- a/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs +++ b/src/modules/Elsa.Telnyx/Activities/IncomingCall.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Extensions; @@ -11,6 +12,9 @@ using Elsa.Workflows.Management.Models; namespace Elsa.Telnyx.Activities; +/// +/// Triggered when an inbound phone call is received for any of the specified source or destination phone numbers. +/// [Activity( "Telnyx", "Telnyx", @@ -18,13 +22,27 @@ namespace Elsa.Telnyx.Activities; Kind = ActivityKind.Trigger)] public class IncomingCall : Trigger { + /// + public IncomingCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// A list of destination numbers to respond to. + /// [Input(Description = "A list of destination numbers to respond to.", UIHint = InputUIHints.MultiText)] public Input> To { get; set; } = default!; + /// + /// A list of source numbers to respond to. + /// [Input(Description = "A list of source numbers to respond to.", UIHint = InputUIHints.MultiText)] public Input> From { get; set; } = default!; - [Input(Description = "Match any inbound calls")] + /// + /// Match any inbound calls. + /// + [Input(Description = "Match any inbound calls.")] public Input CatchAll { get; set; } = default!; /// diff --git a/src/modules/Elsa.Telnyx/Activities/LookupNumber.cs b/src/modules/Elsa.Telnyx/Activities/LookupNumber.cs index 309ba1236..0220d5d5f 100644 --- a/src/modules/Elsa.Telnyx/Activities/LookupNumber.cs +++ b/src/modules/Elsa.Telnyx/Activities/LookupNumber.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Workflows.Core; using Elsa.Workflows.Core.Attributes; @@ -13,6 +14,11 @@ namespace Elsa.Telnyx.Activities; [Activity(Constants.Namespace, "Returns information about the provided phone number.", Kind = ActivityKind.Task)] public class LookupNumber : Activity { + /// + public LookupNumber([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// The phone number to be looked up. /// diff --git a/src/modules/Elsa.Telnyx/Activities/PlayAudio.cs b/src/modules/Elsa.Telnyx/Activities/PlayAudio.cs index 6cd23802e..d2a6886ce 100644 --- a/src/modules/Elsa.Telnyx/Activities/PlayAudio.cs +++ b/src/modules/Elsa.Telnyx/Activities/PlayAudio.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Attributes; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; @@ -14,20 +15,47 @@ using Refit; namespace Elsa.Telnyx.Activities; +/// [FlowNode("Playback started", "Disconnected")] public class FlowPlayAudio : PlayAudioBase { + /// + public FlowPlayAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// protected override ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Playback started"); + + /// protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected"); } +/// public class PlayAudio : PlayAudioBase { + /// + public PlayAudio([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The to execute when audio playback has started. + /// [Port] public IActivity? PlaybackStarted { get; set; } + + /// + /// The to execute when the call was no longer active. + /// [Port] public IActivity? Disconnected { get; set; } + /// protected override async ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(PlaybackStarted, OnCompletedAsync); + + /// protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync); + + private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); } /// @@ -38,6 +66,11 @@ public class PlayAudio : PlayAudioBase [WebhookDriven(WebhookEventTypes.CallPlaybackStarted)] public abstract class PlayAudioBase : ActivityBase, IBookmarksPersistedHandler { + /// + protected PlayAudioBase([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// @@ -119,8 +152,16 @@ public abstract class PlayAudioBase : ActivityBase, IBookmarksPersistedHandler /// protected override void Execute(ActivityExecutionContext context) => context.CreateBookmark(new WebhookEventBookmarkPayload(WebhookEventTypes.CallPlaybackStarted), ResumeAsync); + /// + /// Called when playback has started. + /// protected abstract ValueTask HandlePlaybackStartedAsync(ActivityExecutionContext context); + + + /// + /// Called when the call was no longer active. + /// protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context); - protected async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); + private async ValueTask ResumeAsync(ActivityExecutionContext context) => await HandlePlaybackStartedAsync(context); } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/SpeakText.cs b/src/modules/Elsa.Telnyx/Activities/SpeakText.cs index c130c60cc..41504653d 100644 --- a/src/modules/Elsa.Telnyx/Activities/SpeakText.cs +++ b/src/modules/Elsa.Telnyx/Activities/SpeakText.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Attributes; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; @@ -13,10 +14,18 @@ using Refit; namespace Elsa.Telnyx.Activities; +/// +/// Convert text to speech and play it back on the call. +/// [Activity(Constants.Namespace, "Convert text to speech and play it back on the call.", Kind = ActivityKind.Task)] [WebhookDriven(WebhookEventTypes.CallSpeakEnded)] public abstract class SpeakTextBase : ActivityBase { + /// + protected SpeakTextBase([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// @@ -105,31 +114,56 @@ public abstract class SpeakTextBase : ActivityBase } } + /// + /// Called when the call was no longer active. + /// protected abstract ValueTask HandleDisconnected(ActivityExecutionContext context); + + /// + /// Called when speaking has finished. + /// protected abstract ValueTask HandleFinishedSpeaking(ActivityExecutionContext context); private async ValueTask ResumeAsync(ActivityExecutionContext context) => await HandleFinishedSpeaking(context); } +/// [FlowNode("Finished speaking", "Disconnected")] public class FlowSpeakText : SpeakTextBase { + /// + public FlowSpeakText([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// protected override async ValueTask HandleDisconnected(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Disconnected"); + + /// protected override async ValueTask HandleFinishedSpeaking(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Finished speaking"); } +/// public class SpeakText : SpeakTextBase { - [Port]public IActivity? FinishedSpeaking { get; set; } - [Port]public IActivity? Disconnected { get; set; } + /// + public SpeakText([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } - protected override async ValueTask HandleDisconnected(ActivityExecutionContext context) - { - await context.ScheduleActivityAsync(Disconnected); - } + /// + /// The to execute when speaking has finished. + /// + [Port]public IActivity? FinishedSpeaking { get; set; } + + /// + /// The to execute when the call was no longer active. + /// + [Port]public IActivity? Disconnected { get; set; } - protected override async ValueTask HandleFinishedSpeaking(ActivityExecutionContext context) - { - await context.ScheduleActivityAsync(FinishedSpeaking); - } + /// + protected override async ValueTask HandleDisconnected(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected); + + /// + protected override async ValueTask HandleFinishedSpeaking(ActivityExecutionContext context) => await context.ScheduleActivityAsync(FinishedSpeaking); } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/StartRecording.cs b/src/modules/Elsa.Telnyx/Activities/StartRecording.cs index c4fb03f36..95ee46c98 100644 --- a/src/modules/Elsa.Telnyx/Activities/StartRecording.cs +++ b/src/modules/Elsa.Telnyx/Activities/StartRecording.cs @@ -1,4 +1,7 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Attributes; +using Elsa.Telnyx.Bookmarks; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Telnyx.Extensions; using Elsa.Telnyx.Payloads.Call; @@ -12,28 +15,61 @@ using Refit; namespace Elsa.Telnyx.Activities; +/// [FlowNode("Recording finished", "Disconnected")] public class FlowStartRecording : StartRecordingBase { + /// + public FlowStartRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected"); + + /// protected override ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Recording finished"); } +/// public class StartRecording : StartRecordingBase { + /// + public StartRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The to execute when recording has finished. + /// [Port] public IActivity? RecordingFinished { get; set; } + + /// + /// The to executed when the call was no longer active. + /// [Port] public IActivity? Disconnected { get; set; } + /// protected override async ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(RecordingFinished, OnCompletedAsync); + + /// protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync); + + private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); } /// /// Start recording the call. /// [Activity(Constants.Namespace, "Start recording the call.", Kind = ActivityKind.Task)] +[WebhookDriven(WebhookEventTypes.CallRecordingSaved)] public abstract class StartRecordingBase : ActivityBase { + /// + protected StartRecordingBase(string? source = default, int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// @@ -87,7 +123,8 @@ public abstract class StartRecordingBase : ActivityBase + /// Called when the recording was saved. + /// protected abstract ValueTask HandleCallRecordingSavedAsync(ActivityExecutionContext context); + + + /// + /// Called when the call was no longer active. + /// protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context); - protected async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); private async ValueTask ResumeAsync(ActivityExecutionContext context) { diff --git a/src/modules/Elsa.Telnyx/Activities/StopAudioPlayback.cs b/src/modules/Elsa.Telnyx/Activities/StopAudioPlayback.cs index 325704027..dece1896a 100644 --- a/src/modules/Elsa.Telnyx/Activities/StopAudioPlayback.cs +++ b/src/modules/Elsa.Telnyx/Activities/StopAudioPlayback.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Telnyx.Extensions; using Elsa.Workflows.Core; @@ -10,29 +11,55 @@ using Refit; namespace Elsa.Telnyx.Activities; -[FlowNode("Playback ended", "Disconnected")] +/// +[FlowNode("Done", "Disconnected")] public class FlowStopAudioPlayback : StopAudioPlaybackBase { - protected override ValueTask HandlePlaybackEndedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Playback ended"); + /// + public FlowStopAudioPlayback([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + protected override ValueTask HandleDoneAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Done"); + + /// protected override ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => context.CompleteActivityWithOutcomesAsync("Disconnected"); } +/// public class StopAudioPlayback : StopAudioPlaybackBase { - [Port] public IActivity? PlaybackEnded { get; set; } + /// + public StopAudioPlayback([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The to execute when the call was no longer active. + /// [Port] public IActivity? Disconnected { get; set; } - protected override async ValueTask HandlePlaybackEndedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(PlaybackEnded, OnCompletedAsync); + /// + protected override async ValueTask HandleDoneAsync(ActivityExecutionContext context) => await context.CompleteActivityAsync(); + + /// protected override async ValueTask HandleDisconnectedAsync(ActivityExecutionContext context) => await context.ScheduleActivityAsync(Disconnected, OnCompletedAsync); + + private async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); } /// /// Stop audio playback. /// [Activity(Constants.Namespace, Description = "Stop audio playback.", Kind = ActivityKind.Task)] -[FlowNode("Playback ended", "Disconnected")] public abstract class StopAudioPlaybackBase : ActivityBase { + /// + protected StopAudioPlaybackBase(string? source = default, int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// @@ -53,6 +80,7 @@ public abstract class StopAudioPlaybackBase : ActivityBase )] public Input Stop { get; set; } = new("all"); + /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var request = new StopAudioPlaybackRequest(Stop.Get(context)); @@ -62,18 +90,21 @@ public abstract class StopAudioPlaybackBase : ActivityBase try { await telnyxClient.Calls.StopAudioPlaybackAsync(callControlId, request, context.CancellationToken); - context.CreateBookmark(ResumeAsync); + await HandleDoneAsync(context); } catch (ApiException e) { if (!await e.CallIsNoLongerActiveAsync()) throw; - await context.CompleteActivityWithOutcomesAsync("Disconnected"); + await HandleDisconnectedAsync(context); } } - - protected abstract ValueTask HandlePlaybackEndedAsync(ActivityExecutionContext context); + + /// + /// Called when audio playback is stopping. + /// + protected abstract ValueTask HandleDoneAsync(ActivityExecutionContext context); + /// + /// Called when the call was no longer active. + /// protected abstract ValueTask HandleDisconnectedAsync(ActivityExecutionContext context); - protected async ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) => await context.CompleteActivityAsync(); - - private async ValueTask ResumeAsync(ActivityExecutionContext context) => await context.CompleteActivityWithOutcomesAsync("Playback ended"); } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/StopRecording.cs b/src/modules/Elsa.Telnyx/Activities/StopRecording.cs index 2446a35b1..92aad50a3 100644 --- a/src/modules/Elsa.Telnyx/Activities/StopRecording.cs +++ b/src/modules/Elsa.Telnyx/Activities/StopRecording.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Telnyx.Extensions; using Elsa.Workflows.Core; @@ -16,6 +17,11 @@ namespace Elsa.Telnyx.Activities; [FlowNode("Recording stopped", "Disconnected")] public class StopRecording : ActivityBase { + /// + public StopRecording([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// @@ -44,6 +50,4 @@ public class StopRecording : ActivityBase await context.CompleteActivityWithOutcomesAsync("Disconnected"); } } - - private static string? EmptyToNull(string? value) => value is "" ? null : value; } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Activities/TransferCall.cs b/src/modules/Elsa.Telnyx/Activities/TransferCall.cs index 587b3cd62..c97f49159 100644 --- a/src/modules/Elsa.Telnyx/Activities/TransferCall.cs +++ b/src/modules/Elsa.Telnyx/Activities/TransferCall.cs @@ -1,4 +1,5 @@ -using Elsa.Telnyx.Client.Models; +using System.Runtime.CompilerServices; +using Elsa.Telnyx.Client.Models; using Elsa.Telnyx.Client.Services; using Elsa.Telnyx.Extensions; using Elsa.Telnyx.Payloads.Call; @@ -18,6 +19,11 @@ namespace Elsa.Telnyx.Activities; [FlowNode("Transferred", "Hangup", "Disconnected")] public class TransferCall : Activity { + /// + public TransferCall([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// Unique identifier and token for controlling the call. /// diff --git a/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs b/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs index c5638f557..c400948fc 100644 --- a/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs +++ b/src/modules/Elsa.Telnyx/Activities/WebhookEvent.cs @@ -1,5 +1,8 @@ using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; +using Elsa.Telnyx.Attributes; using Elsa.Telnyx.Bookmarks; using Elsa.Telnyx.Helpers; using Elsa.Telnyx.Models; @@ -17,22 +20,25 @@ namespace Elsa.Telnyx.Activities; [Browsable(false)] public class WebhookEvent : ActivityBase { + /// [JsonConstructor] - public WebhookEvent() + public WebhookEvent([CallerFilePath]string? source = default, [CallerLineNumber]int? line = default) : base(source, line) { } - public WebhookEvent(string eventType, Variable result) + /// + public WebhookEvent(string eventType, string activityTypeName, Variable result, int version = 1, [CallerFilePath]string? source = default, [CallerLineNumber]int? line = default) + : base(activityTypeName, version, source, line) { - EventType = new (eventType); + EventType = eventType; Result = new(result); } /// /// The Telnyx webhook event type to listen for. /// - [Input(Description = "The Telnyx webhook event type to listen for")] - public Input EventType { get; set; } = default!; + [Description("The Telnyx webhook event type to listen for")] + public string EventType { get; set; } = default!; /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) @@ -41,9 +47,10 @@ public class WebhookEvent : ActivityBase await Resume(context); else { - var eventType = context.Get(EventType)!; + var eventType = EventType; var payload = new WebhookEventBookmarkPayload(eventType); - context.CreateBookmark(payload, Resume); + + context.CreateBookmark(new CreateBookmarkOptions(payload, Resume, Type)); } } diff --git a/src/modules/Elsa.Telnyx/Bookmarks/CallBridgedBookmarkPayload.cs b/src/modules/Elsa.Telnyx/Bookmarks/CallBridgedBookmarkPayload.cs new file mode 100644 index 000000000..4cf148b22 --- /dev/null +++ b/src/modules/Elsa.Telnyx/Bookmarks/CallBridgedBookmarkPayload.cs @@ -0,0 +1,3 @@ +namespace Elsa.Telnyx.Bookmarks; + +public record CallBridgedBookmarkPayload(string CallControlId); \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Client/Models/Responses.cs b/src/modules/Elsa.Telnyx/Client/Models/Responses.cs index bd88f2aff..45f6dfb1e 100644 --- a/src/modules/Elsa.Telnyx/Client/Models/Responses.cs +++ b/src/modules/Elsa.Telnyx/Client/Models/Responses.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Dahomey.Json.Util; namespace Elsa.Telnyx.Client.Models; @@ -10,7 +11,13 @@ public record DialResponse( string CallSessionId, bool IsAlive, string RecordType -); +) +{ + [JsonConstructor] + public DialResponse() : this(default!, default!, default!, default, default!) + { + } +} public record NumberLookupResponse( CallerName CallerName, @@ -21,7 +28,13 @@ public record NumberLookupResponse( string PhoneNumber, Portability Portability, string RecordType -); +) +{ + [JsonConstructor] + public NumberLookupResponse() : this(default!, default!, default!, default!, default!, default!, default!, default!) + { + } +} public record Portability( string Altspid, @@ -37,7 +50,13 @@ public record Portability( string SpidCarrierName, string SpidCarrierType, string State -); +) +{ + [JsonConstructor] + public Portability() : this(default!, default!, default!, default!, default!, default!, default!, default, default!, default!, default!, default!, default!) + { + } +} public record Carrier( string ErrorCode, @@ -45,7 +64,13 @@ public record Carrier( int MobileNetworkCode, string Name, string Type -); +) +{ + [JsonConstructor] + public Carrier() : this(default!, default!, default, default!, default!) + { + } +} public record CallerName { diff --git a/src/modules/Elsa.Telnyx/Handlers/TriggerBridgeCallActivities.cs b/src/modules/Elsa.Telnyx/Handlers/TriggerBridgeCallActivities.cs new file mode 100644 index 000000000..c008f158a --- /dev/null +++ b/src/modules/Elsa.Telnyx/Handlers/TriggerBridgeCallActivities.cs @@ -0,0 +1,43 @@ +using Elsa.Mediator.Services; +using Elsa.Telnyx.Activities; +using Elsa.Telnyx.Bookmarks; +using Elsa.Telnyx.Events; +using Elsa.Telnyx.Extensions; +using Elsa.Telnyx.Payloads.Abstract; +using Elsa.Telnyx.Payloads.Call; +using Elsa.Workflows.Core; +using Elsa.Workflows.Core.Helpers; +using Elsa.Workflows.Runtime.Services; +using Microsoft.Extensions.Logging; + +namespace Elsa.Telnyx.Handlers; + +/// +/// Triggers all workflows starting with or blocked on a activity. +/// +internal class TriggerBridgeCallActivities : INotificationHandler +{ + private readonly IWorkflowRuntime _workflowRuntime; + private readonly ILogger _logger; + + public TriggerBridgeCallActivities(IWorkflowRuntime workflowRuntime, ILogger logger) + { + _workflowRuntime = workflowRuntime; + _logger = logger; + } + + public async Task HandleAsync(TelnyxWebhookReceived notification, CancellationToken cancellationToken) + { + var webhook = notification.Webhook; + var payload = webhook.Data.Payload; + + if (payload is not CallBridgedPayload callBridgedPayload) + return; + + var correlationId = ((Payload)webhook.Data.Payload).GetCorrelationId();; + var activityTypeName = ActivityTypeNameHelper.GenerateTypeName(); + var input = new Dictionary().AddInput(callBridgedPayload); + var callBridgedBookmarkPayload = new CallBridgedBookmarkPayload(callBridgedPayload.CallControlId); + await _workflowRuntime.TriggerWorkflowsAsync(activityTypeName, callBridgedBookmarkPayload, new TriggerWorkflowsRuntimeOptions(correlationId, input), cancellationToken); + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Handlers/TriggerIncomingCallActivities.cs b/src/modules/Elsa.Telnyx/Handlers/TriggerIncomingCallActivities.cs index 4f25f9975..a79a26769 100644 --- a/src/modules/Elsa.Telnyx/Handlers/TriggerIncomingCallActivities.cs +++ b/src/modules/Elsa.Telnyx/Handlers/TriggerIncomingCallActivities.cs @@ -34,6 +34,9 @@ internal class TriggerIncomingCallActivities : INotificationHandler(); var input = new Dictionary().AddInput(webhook); diff --git a/src/modules/Elsa.Telnyx/Handlers/TriggerWebhookActivities.cs b/src/modules/Elsa.Telnyx/Handlers/TriggerWebhookActivities.cs index f7a0881f0..9fe70afb4 100644 --- a/src/modules/Elsa.Telnyx/Handlers/TriggerWebhookActivities.cs +++ b/src/modules/Elsa.Telnyx/Handlers/TriggerWebhookActivities.cs @@ -7,6 +7,7 @@ using Elsa.Telnyx.Events; using Elsa.Telnyx.Extensions; using Elsa.Telnyx.Payloads.Abstract; using Elsa.Workflows.Core; +using Elsa.Workflows.Core.Helpers; using Elsa.Workflows.Runtime.Services; using Microsoft.Extensions.Logging; @@ -31,8 +32,7 @@ internal class TriggerWebhookActivities : INotificationHandler()?.ActivityType}"; + var activityType = payload.GetType().GetCustomAttribute()?.ActivityType; if (activityType == null) return; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallAnsweredPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallAnsweredPayload.cs index 0f57b7d8d..f29b4328e 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallAnsweredPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallAnsweredPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallAnswered, ActivityTypeName, "Call Answered", "Triggered when an incoming call is answered.")] +[Webhook(WebhookEventTypes.CallAnswered, WebhookActivityTypeNames.CallAnswered, "Call Answered", "Triggered when an incoming call is answered.")] public sealed record CallAnsweredPayload : CallPayload { - public const string ActivityTypeName = "CallAnswered"; public string From { get; init; } = default!; public string To { get; init; } = default!; public string State { get; init; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallBridgedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallBridgedPayload.cs index b5e0b2136..42d5f121d 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallBridgedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallBridgedPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallBridged, ActivityTypeName, "Call Bridged", "Triggered when an a call is bridged.")] +[Webhook(WebhookEventTypes.CallBridged, WebhookActivityTypeNames.CallBridged, "Call Bridged", "Triggered when an a call is bridged.")] public sealed record CallBridgedPayload : CallPayload { - public const string ActivityTypeName = "CallBridged"; public string From { get; init; } = default!; public string To { get; init; } = default!; public string State { get; init; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallDtmfReceivedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallDtmfReceivedPayload.cs index 36d2f3e42..d1629dfcb 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallDtmfReceivedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallDtmfReceivedPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallDtmfReceived, ActivityTypeName, "Call DTMF Received", "Triggered when DTMF input is received.")] +[Webhook(WebhookEventTypes.CallDtmfReceived, WebhookActivityTypeNames.CallDtmfReceived, "Call DTMF Received", "Triggered when DTMF input is received.")] public sealed record CallDtmfReceivedPayload : CallPayload { - public const string ActivityTypeName = "CallDtmfReceived"; public string Digit { get; set; } = default!; public string From { get; set; } = default!; public string To { get; set; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallGatherEndedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallGatherEndedPayload.cs index 2d688a0f6..ccc2dccb1 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallGatherEndedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallGatherEndedPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallGatherEnded, ActivityTypeName, "Call Gather Ended", "Triggered when an call gather has ended.")] +[Webhook(WebhookEventTypes.CallGatherEnded, WebhookActivityTypeNames.CallGatherEnded, "Call Gather Ended", "Triggered when an call gather has ended.")] public sealed record CallGatherEndedPayload : CallPayload { - public const string ActivityTypeName = "CallGatherEnded"; public string Digits { get; set; } = default!; public string From { get; set; } = default!; public string To { get; set; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallHangupPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallHangupPayload.cs index 1c9850789..8f3a7e9ce 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallHangupPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallHangupPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallHangup, ActivityTypeName, "Call Hangup", "Triggered when an incoming call was hangup.")] +[Webhook(WebhookEventTypes.CallHangup, WebhookActivityTypeNames.CallHangup, "Call Hangup", "Triggered when an incoming call was hangup.")] public sealed record CallHangupPayload : CallPayload { - public const string ActivityTypeName = "CallHangup"; public DateTimeOffset StartTime { get; init; } public DateTimeOffset EndTime { get; init; } public string SipHangupCause { get; init; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallInitiatedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallInitiatedPayload.cs index d06ec4100..cd17d6086 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallInitiatedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallInitiatedPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallInitiated, ActivityTypeName, "Call Initiated", "Triggered when an incoming call is received.")] +[Webhook(WebhookEventTypes.CallInitiated, WebhookActivityTypeNames.CallInitiated, "Call Initiated", "Triggered when an incoming call is received.")] public sealed record CallInitiatedPayload : CallPayload -{ - public const string ActivityTypeName = "CallInitiated"; +{ public string Direction { get; init; } = default!; public string State { get; init; } = default!; public string To { get; init; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallMachineGreetingEnded.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallMachineGreetingEnded.cs index cd0f75813..0e05fddb3 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallMachineGreetingEnded.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallMachineGreetingEnded.cs @@ -2,8 +2,5 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallMachineGreetingEnded, ActivityTypeName, "Call Machine Greeting Ended", "Triggered when a machine greeting has ended.")] -public sealed record CallMachineGreetingEnded : CallMachineGreetingEndedBase -{ - public const string ActivityTypeName = "CallMachineGreetingEnded"; -} \ No newline at end of file +[Webhook(WebhookEventTypes.CallMachineGreetingEnded, WebhookActivityTypeNames.CallMachineGreetingEnded, "Call Machine Greeting Ended", "Triggered when a machine greeting has ended.")] +public sealed record CallMachineGreetingEnded : CallMachineGreetingEndedBase; \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumDetectionEnded.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumDetectionEnded.cs index d1706ad5e..69c3c67e8 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumDetectionEnded.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumDetectionEnded.cs @@ -2,8 +2,12 @@ using Elsa.Telnyx.Attributes; namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallMachinePremiumDetectionEnded, ActivityTypeName, "Call Machine Premium Detection Ended", "Triggered when machine detection has ended.")] +[Webhook( + WebhookEventTypes.CallMachinePremiumDetectionEnded, + WebhookActivityTypeNames.CallMachinePremiumDetectionEnded, + "Call Machine Premium Detection Ended", + "Triggered when machine detection has ended." +)] public sealed record CallMachinePremiumDetectionEnded : CallMachineDetectionEndedBase { - public const string ActivityTypeName = nameof(CallMachinePremiumDetectionEnded); } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumGreetingEnded.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumGreetingEnded.cs index 56499c094..1ed9a9238 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumGreetingEnded.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallMachinePremiumGreetingEnded.cs @@ -2,8 +2,10 @@ using Elsa.Telnyx.Attributes; namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallMachinePremiumGreetingEnded, ActivityTypeName, "Call Machine Premium Greeting Ended", "Triggered when a machine greeting has ended.")] -public sealed record CallMachinePremiumGreetingEnded : CallMachineGreetingEndedBase -{ - public const string ActivityTypeName = nameof(CallMachinePremiumGreetingEnded); -} \ No newline at end of file +[Webhook( + WebhookEventTypes.CallMachinePremiumGreetingEnded, + WebhookActivityTypeNames.CallMachinePremiumGreetingEnded, + "Call Machine Premium Greeting Ended", + "Triggered when a machine greeting has ended." +)] +public sealed record CallMachinePremiumGreetingEnded : CallMachineGreetingEndedBase; \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackEndedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackEndedPayload.cs index 20cd6c38d..700bd93d9 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackEndedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackEndedPayload.cs @@ -2,9 +2,8 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallPlaybackEnded, ActivityTypeName, "Call Playback Ended", "Triggered when an audio playback has ended.")] +[Webhook(WebhookEventTypes.CallPlaybackEnded, WebhookActivityTypeNames.CallPlaybackEnded, "Call Playback Ended", "Triggered when an audio playback has ended.")] public sealed record CallPlaybackEndedPayload : CallPlayback { - public const string ActivityTypeName = "CallPlaybackEnded"; public string Status { get; set; } = default!; } \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackStartedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackStartedPayload.cs index 122c4cc10..ab624a8fc 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackStartedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallPlaybackStartedPayload.cs @@ -2,8 +2,5 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallPlaybackStarted, ActivityTypeName, "Call Playback Started", "Triggered when an audio playback has started.")] -public sealed record CallPlaybackStartedPayload : CallPlayback -{ - public const string ActivityTypeName = "CallPlaybackStarted"; -} \ No newline at end of file +[Webhook(WebhookEventTypes.CallPlaybackStarted, WebhookActivityTypeNames.CallPlaybackStarted, "Call Playback Started", "Triggered when an audio playback has started.")] +public sealed record CallPlaybackStartedPayload : CallPlayback; \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallRecordingSavedPayload.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallRecordingSavedPayload.cs index 3820a1839..b06d0c155 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallRecordingSavedPayload.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallRecordingSavedPayload.cs @@ -2,10 +2,9 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallRecordingSaved, ActivityTypeName, "Call Recording Saved", "Triggered when a recording has been saved.")] +[Webhook(WebhookEventTypes.CallRecordingSaved, WebhookActivityTypeNames.CallRecordingSaved, "Call Recording Saved", "Triggered when a recording has been saved.")] public sealed record CallRecordingSavedPayload : CallPayload { - public const string ActivityTypeName = "CallRecordingSaved"; public string Channels { get; set; } = default!; public CallRecordingUrls PublicRecordingUrls { get; set; } = default!; public CallRecordingUrls RecordingUrls { get; set; } = default!; diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakEnded.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakEnded.cs index 35b50bfd0..940c5612b 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakEnded.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakEnded.cs @@ -2,8 +2,5 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallSpeakEnded, ActivityTypeName, "Call Speak Ended", "Triggered when speaking has ended.")] -public sealed record CallSpeakEnded : CallPayload -{ - public const string ActivityTypeName = "CallSpeakEnded"; -} \ No newline at end of file +[Webhook(WebhookEventTypes.CallSpeakEnded, WebhookActivityTypeNames.CallSpeakEnded, "Call Speak Ended", "Triggered when speaking has ended.")] +public sealed record CallSpeakEnded : CallPayload; \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakStarted.cs b/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakStarted.cs index bb498efed..dbde3fec1 100644 --- a/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakStarted.cs +++ b/src/modules/Elsa.Telnyx/Payloads/Call/CallSpeakStarted.cs @@ -2,8 +2,5 @@ namespace Elsa.Telnyx.Payloads.Call; -[Webhook(WebhookEventTypes.CallSpeakStarted, ActivityTypeName, "Call Speak Started", "Triggered when speaking has started.")] -public sealed record CallSpeakStarted : CallPayload -{ - public const string ActivityTypeName = "CallSpeakStarted"; -} \ No newline at end of file +[Webhook(WebhookEventTypes.CallSpeakStarted, WebhookActivityTypeNames.CallSpeakStarted, "Call Speak Started", "Triggered when speaking has started.")] +public sealed record CallSpeakStarted : CallPayload; \ No newline at end of file diff --git a/src/modules/Elsa.Telnyx/Providers/WebhookEventActivityProvider.cs b/src/modules/Elsa.Telnyx/Providers/WebhookEventActivityProvider.cs index 79ea53ba2..2584e78c6 100644 --- a/src/modules/Elsa.Telnyx/Providers/WebhookEventActivityProvider.cs +++ b/src/modules/Elsa.Telnyx/Providers/WebhookEventActivityProvider.cs @@ -39,9 +39,7 @@ public class WebhookEventActivityProvider : IActivityProvider private ActivityDescriptor CreateDescriptor(Type payloadType) { var webhookAttribute = payloadType.GetCustomAttribute() ?? throw new Exception($"No WebhookAttribute found on payload type {payloadType}"); - var ns = Constants.Namespace; var typeName = webhookAttribute.ActivityType; - var fullTypeName = $"{ns}.{typeName}"; var displayNameAttr = payloadType.GetCustomAttribute(); var displayName = displayNameAttr?.DisplayName ?? webhookAttribute.DisplayName; var categoryAttr = payloadType.GetCustomAttribute(); @@ -51,7 +49,7 @@ public class WebhookEventActivityProvider : IActivityProvider return new() { - TypeName = fullTypeName, + TypeName = typeName, Version = 1, DisplayName = displayName, Description = description, @@ -62,8 +60,8 @@ public class WebhookEventActivityProvider : IActivityProvider Constructor = context => { var activity = _activityFactory.Create(context); - activity.Type = fullTypeName; - activity.EventType = new Input(webhookAttribute!.EventType); + activity.Type = typeName; + activity.EventType = webhookAttribute!.EventType; return activity; } diff --git a/src/modules/Elsa.Telnyx/WebhookActivityTypeNames.cs b/src/modules/Elsa.Telnyx/WebhookActivityTypeNames.cs new file mode 100644 index 000000000..e0b939045 --- /dev/null +++ b/src/modules/Elsa.Telnyx/WebhookActivityTypeNames.cs @@ -0,0 +1,20 @@ +namespace Elsa.Telnyx; + +public static class WebhookActivityTypeNames +{ + public const string CallAnswered = $"{Constants.Namespace}.{nameof(CallAnswered)}"; + public const string CallBridged = $"{Constants.Namespace}.{nameof(CallBridged)}"; + public const string CallDtmfReceived = $"{Constants.Namespace}.{nameof(CallDtmfReceived)}"; + public const string CallGatherEnded = $"{Constants.Namespace}.{nameof(CallGatherEnded)}"; + public const string CallHangup = $"{Constants.Namespace}.{nameof(CallHangup)}"; + public const string CallInitiated = $"{Constants.Namespace}.{nameof(CallInitiated)}"; + public const string CallMachineGreetingEnded = $"{Constants.Namespace}.{nameof(CallMachineGreetingEnded)}"; + public const string CallMachinePremiumGreetingEnded = $"{Constants.Namespace}.{nameof(CallMachinePremiumGreetingEnded)}"; + public const string CallMachineDetectionEnded = $"{Constants.Namespace}.{nameof(CallMachineDetectionEnded)}"; + public const string CallMachinePremiumDetectionEnded = $"{Constants.Namespace}.{nameof(CallMachinePremiumDetectionEnded)}"; + public const string CallPlaybackStarted = $"{Constants.Namespace}.{nameof(CallPlaybackStarted)}"; + public const string CallPlaybackEnded = $"{Constants.Namespace}.{nameof(CallPlaybackEnded)}"; + public const string CallRecordingSaved = $"{Constants.Namespace}.{nameof(CallRecordingSaved)}"; + public const string CallSpeakStarted = $"{Constants.Namespace}.{nameof(CallSpeakStarted)}"; + public const string CallSpeakEnded = $"{Constants.Namespace}.{nameof(CallSpeakEnded)}"; +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Break.cs b/src/modules/Elsa.Workflows.Core/Activities/Break.cs index 4efc328fe..d636eaec8 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Break.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Break.cs @@ -1,12 +1,21 @@ +using System.Runtime.CompilerServices; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Signals; namespace Elsa.Workflows.Core.Activities; -[Activity("Elsa", "Control Flow", "Break out of a loop")] +/// +/// Break out of a loop. +/// +[Activity("Elsa", "Control Flow", "Break out of a loop.")] public class Break : Activity { + /// + public Break([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { await context.SendSignalAsync(new BreakSignal()); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Complete.cs b/src/modules/Elsa.Workflows.Core/Activities/Complete.cs index 23b0c15bc..8b333a742 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Complete.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Complete.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Activities.Flowchart.Models; @@ -16,27 +17,33 @@ public class Complete : Activity { /// [JsonConstructor] - public Complete() - { - } - - /// - public Complete(params string[] outcomes) : this(new Input>(outcomes)) + public Complete([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } /// - public Complete(Func> outcomes) : this(new Input>(outcomes)) + public Complete(IEnumerable outcomes, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input>(outcomes.ToList()), source, line) { } /// - public Complete(Func outcome) : this(context => new[] { outcome(context) }) + public Complete(Func> outcomes, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input>(outcomes), source, line) { } /// - public Complete(Input> outcomes) => Outcomes = outcomes; + public Complete(Func outcome, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(context => new[] { outcome(context) }, source, line) + { + } + + /// + public Complete(Input> outcomes, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) + { + Outcomes = outcomes; + } /// /// The outcome or set of outcomes to complete this activity with. diff --git a/src/modules/Elsa.Workflows.Core/Activities/Composite.cs b/src/modules/Elsa.Workflows.Core/Activities/Composite.cs index 3f5e6cede..b501fc107 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Composite.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Composite.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Activities.Flowchart.Models; @@ -15,7 +16,7 @@ namespace Elsa.Workflows.Core.Activities; public abstract class Composite : ActivityBase { /// - protected Composite() + protected Composite(string? source = default, int? line = default) : base(source, line) { OnSignalReceived(OnCompleteCompositeSignal); } @@ -70,29 +71,73 @@ public abstract class Composite : ActivityBase private async ValueTask OnCompleteCompositeSignal(CompleteCompositeSignal signal, SignalContext context) { - var activityExecutionContext = context.ReceiverActivityExecutionContext; - - // Remove the existing completed handler. - activityExecutionContext.WorkflowExecutionContext.PopCompletionCallback(activityExecutionContext, Root); - - // Complete this activity. - await activityExecutionContext.CompleteActivityAsync(signal.Result); + // Complete the sender first so that it notifies its parents to complete. + await context.SenderActivityExecutionContext.CompleteActivityAsync(); + + // Then complete this activity. + await context.ReceiverActivityExecutionContext.CompleteActivityAsync(signal.Result); context.StopPropagation(); } - protected static Inline Inline(Func activity) => new(activity); - protected static Inline Inline(Func activity) => new(activity); - protected static Inline Inline(Action activity) => new(activity); - protected static Inline Inline(Action activity) => new(activity); - protected static Inline Inline(Func> activity, MemoryBlockReference? output = default) => new(activity, output); - protected static Inline Inline(Func> activity, MemoryBlockReference? output = default) => new(activity, output); - protected static Inline Inline(Func activity, MemoryBlockReference? output = default) => new(activity, output); - protected static Inline Inline(Func activity, MemoryBlockReference? output = default) => new(activity, output); + /// + /// Creates a new activity. + /// + protected static Inline From(Func activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline From(Func activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline From(Action activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline From(Action activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline From(Func> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, output, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline From(Func> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, output, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline From(Func activity, MemoryBlockReference? output, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, output, source, line); + + /// + /// Creates a new activity. + /// + protected static Inline Inline(Func activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(activity, output, source, line); - protected static SetVariable SetVariable(Variable variable, T value) => new(variable, value); - protected static SetVariable SetVariable(Variable variable, Func value) => new(variable, value); - protected static SetVariable SetVariable(Variable variable, Func value) => new(variable, value); - protected static SetVariable SetVariable(Variable variable, Variable value) => new(variable, value); + /// + /// Creates a new activity. + /// + protected static SetVariable SetVariable(Variable variable, T value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(variable, value, source, line); + + /// + /// Creates a new activity. + /// + protected static SetVariable SetVariable(Variable variable, Func value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(variable, value, source, line); + + /// + /// Creates a new activity. + /// + protected static SetVariable SetVariable(Variable variable, Func value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(variable, value, source, line); + + /// + /// Creates a new activity. + /// + protected static SetVariable SetVariable(Variable variable, Variable value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(variable, value, source, line); } /// @@ -101,7 +146,7 @@ public abstract class Composite : ActivityBase public abstract class Composite : ActivityBase { /// - protected Composite() + protected Composite([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { OnSignalReceived(OnCompleteCompositeSignal); } @@ -128,18 +173,18 @@ public abstract class Composite : ActivityBase { } - private async ValueTask OnRootCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) - { - await OnCompletedAsync(context, childContext); - await context.CompleteActivityAsync(); - } - + /// + /// Override this method to handle the completion event for this composite activity. + /// protected virtual ValueTask OnCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) { OnCompleted(context, childContext); return new(); } + /// + /// Override this method to handle the completion event for this composite activity. + /// protected virtual void OnCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) { } @@ -154,6 +199,12 @@ public abstract class Composite : ActivityBase /// protected async Task CompleteAsync(ActivityExecutionContext context, params string[] outcomes) => await CompleteAsync(context, new Outcomes(outcomes)); + private async ValueTask OnRootCompletedAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) + { + await OnCompletedAsync(context, childContext); + await context.CompleteActivityAsync(); + } + private async ValueTask OnCompleteCompositeSignal(CompleteCompositeSignal signal, SignalContext context) { var activityExecutionContext = context.ReceiverActivityExecutionContext; @@ -166,12 +217,43 @@ public abstract class Composite : ActivityBase context.StopPropagation(); } + /// + /// Creates a new activity. + /// protected static Inline From(Func activity) => new(activity); + + /// + /// Creates a new activity. + /// protected static Inline From(Func activity) => new(activity); + + /// + /// Creates a new activity. + /// protected static Inline From(Action activity) => new(activity); + + /// + /// Creates a new activity. + /// protected static Inline From(Action activity) => new(activity); + + /// + /// Creates a new activity. + /// protected static Inline From(Func> activity, MemoryBlockReference? output = default) => new(activity, output); + + /// + /// Creates a new activity. + /// protected static Inline From(Func> activity, MemoryBlockReference? output = default) => new(activity, output); + + /// + /// Creates a new activity. + /// protected static Inline From(Func activity, MemoryBlockReference? output = default) => new(activity, output); + + /// + /// Creates a new activity. + /// protected static Inline From(Func activity, MemoryBlockReference? output = default) => new(activity, output); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Container.cs b/src/modules/Elsa.Workflows.Core/Activities/Container.cs index 1ea8de193..21f9cc90a 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Container.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Container.cs @@ -1,6 +1,5 @@ using System.Collections.ObjectModel; using Elsa.Workflows.Core.Attributes; -using Elsa.Workflows.Core.Behaviors; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; @@ -11,23 +10,22 @@ namespace Elsa.Workflows.Core.Activities; /// public abstract class Container : ActivityBase, IContainer { - protected Container() + /// + protected Container(string? source = default, int? line = default) : base(source, line) { } - - protected Container(params IActivity[] activities) - { - Activities = activities; - } - - protected Container(ICollection variables, params IActivity[] activities) : this(activities) - { - Variables = variables; - } + /// + /// The s to execute. + /// [Port] public ICollection Activities { get; set; } = new HashSet(); + + /// + /// The variables available to this scope. + /// public ICollection Variables { get; set; } = new Collection(); + /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { // Register variables. @@ -37,12 +35,18 @@ public abstract class Container : ActivityBase, IContainer await ScheduleChildrenAsync(context); } + /// + /// Schedule the for execution. + /// protected virtual ValueTask ScheduleChildrenAsync(ActivityExecutionContext context) { ScheduleChildren(context); return ValueTask.CompletedTask; } + /// + /// Schedule the for execution. + /// protected virtual void ScheduleChildren(ActivityExecutionContext context) { } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Event.cs b/src/modules/Elsa.Workflows.Core/Activities/Event.cs index 3d2e5a1f6..f9dfe1abb 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Event.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Event.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; @@ -13,36 +14,41 @@ public class Event : Trigger { /// [JsonConstructor] - public Event() + public Event([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } /// - public Event(string eventName) : this(new Literal(eventName)) + public Event(string eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Literal(eventName), source, line) { } /// - public Event(Func text) : this(new DelegateBlockReference(text)) + public Event(Func text, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new DelegateBlockReference(text), source, line) { } /// - public Event(Func text) : this(new DelegateBlockReference(text)) + public Event(Func text, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new DelegateBlockReference(text), source, line) { } /// - public Event(Variable variable) => EventName = new Input(variable); + public Event(Variable variable, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + EventName = new Input(variable); /// - public Event(Literal literal) => EventName = new Input(literal); + public Event(Literal literal, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + EventName = new Input(literal); /// - public Event(DelegateBlockReference delegateBlockExpression) => EventName = new Input(delegateBlockExpression); + public Event(DelegateBlockReference delegateBlockExpression, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + EventName = new Input(delegateBlockExpression); /// - public Event(Input eventName) => EventName = eventName; + public Event(Input eventName, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => EventName = eventName; /// /// The name of the event to listen for. diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs index 4d4c534ab..2f0c14efb 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowDecision.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Activities.Flowchart.Attributes; using Elsa.Workflows.Core.Activities.Flowchart.Models; @@ -6,10 +7,18 @@ using Elsa.Workflows.Core.Models; namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; +/// +/// Performs a boolean condition and returns an outcome based on the the result. +/// [FlowNode("True", "False")] [Activity("Elsa", "Flow", "Evaluate a Boolean condition to determine which path to execute next.")] public class FlowDecision : ActivityBase { + /// + public FlowDecision([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// The condition to evaluate. /// diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs index b4623306b..e77d97cef 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowJoin.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Common.Extensions; using Elsa.Workflows.Core.Activities.Flowchart.Contracts; using Elsa.Workflows.Core.Activities.Flowchart.Extensions; @@ -7,10 +8,18 @@ using Elsa.Workflows.Core.Models; namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; +/// +/// Merge multiple branches into a single branch of execution. +/// [Activity("Elsa", "Flow", "Merge multiple branches into a single branch of execution.")] public class FlowJoin : ActivityBase, IJoinNode { - [Input] public Input Mode { get; set; } = new(Models.FlowJoinMode.WaitAll); + /// + public FlowJoin([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + [Input] public Input Mode { get; set; } = new(FlowJoinMode.WaitAll); protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowNode.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowNode.cs index 11461ca59..51bae4c03 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowNode.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowNode.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; @@ -10,6 +11,11 @@ namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; [Activity("Elsa", "Flow", "A simple container that executes the specified activity.")] public class FlowNode : ActivityBase { + /// + public FlowNode([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// The activity to execute. /// diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs index a4f98faf6..d4e3649c0 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/FlowSwitch.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions; using Elsa.Expressions.Models; @@ -9,12 +10,21 @@ using Elsa.Workflows.Core.Models; namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; +/// +/// Evaluates the specified case conditions and schedules the one that evaluates to true. +/// [FlowNode("Default")] [Activity("Elsa", "Flow", "Evaluate a set of case conditions and schedule the activity for a matching case.")] public class FlowSwitch : ActivityBase { + /// + public FlowSwitch([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + [Input(UIHint = "flow-switch-editor")] public ICollection Cases { get; set; } = new List(); + /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { var matchingCase = await FindMatchingCaseAsync(context.ExpressionExecutionContext); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs index 61a868ba7..424d9167e 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Workflows.Core.Activities.Flowchart.Contracts; using Elsa.Workflows.Core.Activities.Flowchart.Extensions; using Elsa.Workflows.Core.Activities.Flowchart.Models; @@ -8,12 +9,16 @@ using Elsa.Workflows.Core.Signals; namespace Elsa.Workflows.Core.Activities.Flowchart.Activities; +/// +/// A flowchart consists of a collection of activities and connections between them. +/// [Activity("Elsa", "Flow", "A flowchart is a collection of activities and connections between them.")] public class Flowchart : Container { internal const string ScopeProperty = "Scope"; - public Flowchart() + /// + public Flowchart([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { OnSignalReceived(OnDescendantCompletedAsync); } diff --git a/src/modules/Elsa.Workflows.Core/Activities/For.cs b/src/modules/Elsa.Workflows.Core/Activities/For.cs index bdaec9f37..b99e9cf7e 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/For.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/For.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; @@ -12,13 +13,15 @@ public class For : ActivityBase { private const string CurrentStepProperty = "CurrentStep"; + /// [JsonConstructor] - public For() + public For([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { Behaviors.Add(this); } - public For(int start, int end) : this() + /// + public For(int start, int end, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) { Start = new Input(start); End = new Input(end); diff --git a/src/modules/Elsa.Workflows.Core/Activities/ForEach.cs b/src/modules/Elsa.Workflows.Core/Activities/ForEach.cs index a7b261899..234628904 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/ForEach.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/ForEach.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; @@ -7,17 +8,22 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Activities; +/// +/// Iterate over a set of values. +/// [Activity("Elsa", "Control Flow", "Iterate over a set of values.")] public class ForEach : ActivityBase { private const string CurrentIndexProperty = "CurrentIndex"; - public ForEach() + /// + public ForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) { Behaviors.Add(this); } - public ForEach(ICollection items) : this() + /// + public ForEach(ICollection items, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) { Items = new Input>(items); } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Fork.cs b/src/modules/Elsa.Workflows.Core/Activities/Fork.cs index 05aaa3aea..3d5c17f35 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Fork.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Fork.cs @@ -1,4 +1,6 @@ using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; using Elsa.Common.Extensions; using Elsa.Workflows.Core.Activities.Flowchart.Models; using Elsa.Workflows.Core.Attributes; @@ -8,14 +10,23 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Activities; +/// +/// Branch execution into multiple branches. +/// [Activity("Elsa", "Control Flow", "Branch execution into multiple branches.")] public class Fork : ActivityBase { + /// + [JsonConstructor] + public Fork([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// Controls when this activity yields control back to its parent activity. /// [Input] - public ForkJoinMode JoinMode { get; set; } = ForkJoinMode.WaitAny; + public ForkJoinMode JoinMode { get; set; } = ForkJoinMode.WaitAll; /// /// The branches to schedule. @@ -24,7 +35,7 @@ public class Fork : ActivityBase public ICollection Branches { get; set; } = new List(); /// - protected override void Execute(ActivityExecutionContext context) => context.ScheduleActivities(Branches.Reverse(), CompleteChildAsync); + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => context.ScheduleActivities(Branches.Reverse(), CompleteChildAsync); private async ValueTask CompleteChildAsync(ActivityExecutionContext context, ActivityExecutionContext childContext) { diff --git a/src/modules/Elsa.Workflows.Core/Activities/ForkJoinMode.cs b/src/modules/Elsa.Workflows.Core/Activities/ForkJoinMode.cs index f6cc7a4a1..baad92c68 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/ForkJoinMode.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/ForkJoinMode.cs @@ -1,7 +1,17 @@ namespace Elsa.Workflows.Core.Activities; +/// +/// Controls when a completes. +/// public enum ForkJoinMode { + /// + /// The completes after all inbound activities have completed. + /// WaitAll, + + /// + /// The completes as soon as any of its inbound activity completes. + /// WaitAny } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/If.cs b/src/modules/Elsa.Workflows.Core/Activities/If.cs index 4f8e31038..014795e3d 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/If.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/If.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; @@ -11,18 +12,20 @@ public class If : ActivityBase { /// [JsonConstructor] - public If() + public If([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } /// - public If(Input condition) => Condition = condition; + public If(Input condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + Condition = condition; /// - public If(Func condition) => Condition = new Input(condition); + public If(Func condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => + Condition = new Input(condition); /// - public If(Func condition) => Condition = new Input(condition); + public If(Func condition, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => Condition = new Input(condition); /// /// The condition to evaluate. @@ -47,7 +50,7 @@ public class If : ActivityBase { var result = context.Get(Condition); var nextNode = result ? Then : Else; - + context.Set(Result, result); await context.ScheduleActivityAsync(nextNode, OnChildCompleted); } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Inline.cs b/src/modules/Elsa.Workflows.Core/Activities/Inline.cs index 06f3c0a87..3ed65a5f4 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Inline.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Inline.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; @@ -12,25 +13,28 @@ public class Inline : Activity { private readonly Func _activity; - public Inline(Func activity) => _activity = activity; + public Inline(Func activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + _activity = activity; + } - public Inline(Func activity) : this(_ => activity()) + public Inline(Func activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(_ => activity(), source, line) { } - public Inline(Action activity) : this(c => + public Inline(Action activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(c => { activity(c); return new ValueTask(); - }) + }, source, line) { } - public Inline(Action activity) : this(c => + public Inline(Action activity, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(c => { activity(); return new ValueTask(); - }) + }, source, line) { } @@ -54,28 +58,32 @@ public class Inline : Activity { private readonly Func> _activity; - public Inline(Func> activity, MemoryBlockReference? output = default) : base(output) + public Inline(Func> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : base(output, source, line) { _activity = activity; } - public Inline(Func> activity, MemoryBlockReference? output = default) : this(_ => activity(), output) + public Inline(Func> activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(_ => activity(), output, source, line) { } - public Inline(Func activity, MemoryBlockReference? output = default) : this(c => + public Inline(Func activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(c => { var result = activity(c); return new ValueTask(result); - }, output) + }, output, source, line) { } - public Inline(Func activity, MemoryBlockReference? output = default) : this(c => + public Inline(Func activity, MemoryBlockReference? output = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(c => { var result = activity(); return new ValueTask(result); - }, output) + }, output, source, line) { } diff --git a/src/modules/Elsa.Workflows.Core/Activities/ParallelForEach.cs b/src/modules/Elsa.Workflows.Core/Activities/ParallelForEach.cs index 0803762df..c342c6553 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/ParallelForEach.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/ParallelForEach.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; @@ -5,11 +6,29 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Activities; +/// +/// Schedule an activity for each item in parallel. +/// +/// [Activity("Elsa", "Control Flow", "Schedule an activity for each item in parallel.")] public class ParallelForEach : Activity { private const string CollectedCountProperty = nameof(CollectedCountProperty); + + /// + public ParallelForEach([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The items to iterate. + /// [Input] public Input> Items { get; set; } = new(Array.Empty()); + + + /// + /// The to execute each iteration. + /// [Port] public IActivity Body { get; set; } = default!; /// diff --git a/src/modules/Elsa.Workflows.Core/Activities/ReadLine.cs b/src/modules/Elsa.Workflows.Core/Activities/ReadLine.cs index e9ccbf7b9..a0afc37ea 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/ReadLine.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/ReadLine.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Implementations; @@ -6,24 +7,31 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Activities; +/// +/// Read a line of text from the console +/// [Activity("Elsa", "Console", "Read a line of text from the console.")] public class ReadLine : Activity { - public ReadLine() + /// + public ReadLine([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public ReadLine(MemoryBlockReference output) : base(output) - { - } - - public ReadLine(Output? output) : base(output) + /// + public ReadLine(MemoryBlockReference output, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(output, source, line) { } + /// + public ReadLine(Output? output, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(output, source, line) + { + } + + /// protected override void Execute(ActivityExecutionContext context) { - var provider = context.GetService() ?? new StandardInStreamProvider(System.Console.In); + var provider = context.GetService() ?? new StandardInStreamProvider(Console.In); var reader = provider.GetTextReader(); var text = reader.ReadLine()!; context.Set(Result, text); diff --git a/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs b/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs index 8ceb4f351..2b8c8ba18 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Runtime.CompilerServices; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Services; @@ -6,25 +7,22 @@ using Elsa.Workflows.Core.Signals; namespace Elsa.Workflows.Core.Activities; +/// +/// Execute a set of activities in sequence. +/// [Category("Workflows")] [Activity("Elsa", "Workflows", "Execute a set of activities in sequence.")] public class Sequence : Container { private const string CurrentIndexProperty = "CurrentIndex"; - - public Sequence() + + /// + public Sequence([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { OnSignalReceived(OnBreak); } - public Sequence(params IActivity[] activities) : base(activities) - { - } - - public Sequence(ICollection variables, params IActivity[] activities) : base(variables, activities) - { - } - + /// protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContext context) { await HandleItemAsync(context); @@ -53,7 +51,7 @@ public class Sequence : Container private void OnBreak(BreakSignal signal, SignalContext context) { - // Clear any scheduled child completion callbacks, since we no longer want to schedule any sibling. + // Clear any scheduled child completion callbacks, since we no longer want to schedule any siblings. context.ReceiverActivityExecutionContext.ClearCompletionCallbacks(); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/SetName.cs b/src/modules/Elsa.Workflows.Core/Activities/SetName.cs index 5d1b0a573..0eeaeab59 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/SetName.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/SetName.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; @@ -14,11 +15,11 @@ public class SetName : Activity internal static readonly object WorkflowInstanceNameKey = new(); [JsonConstructor] - public SetName() + public SetName([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public SetName(Input value) + public SetName(Input value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) { Value = value; } diff --git a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs index eecfa91b0..0271adec0 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/SetVariable.cs @@ -1,43 +1,65 @@ using System.ComponentModel; +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Models; namespace Elsa.Workflows.Core.Activities; +/// +/// Assign a workflow variable a value. +/// [Browsable(false)] -[Activity("Elsa", "Primitives", "Set a workflow variable to a given value.")] +[Activity("Elsa", "Primitives", "Assign a workflow variable a value.")] public class SetVariable : Activity { - public SetVariable() + /// + public SetVariable([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public SetVariable(Variable variable, Input value) + /// + public SetVariable(Variable variable, Input value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) { Variable = variable; Value = value; } - - public SetVariable(Variable variable, Variable value) : this(variable, new Input(value)) - { - } - - public SetVariable(Variable variable, Func value) : this(variable, new Input(value)) - { - } - - public SetVariable(Variable variable, Func value) : this(variable, new Input(value)) - { - } - - public SetVariable(Variable variable, T value) : this(variable, new Input(value)) + + /// + public SetVariable(Variable variable, Variable value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(variable, new Input(value), source, line) { } - [Input] public Input Value { get; set; } = new(new Literal()); + /// + public SetVariable(Variable variable, Func value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(variable, new Input(value), source, line) + { + } + + /// + public SetVariable(Variable variable, Func value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(variable, new Input(value), source, line) + { + } + + /// + public SetVariable(Variable variable, T value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(variable, new Input(value), source, line) + { + } + + /// + /// The variable to assign the value to. + /// public Variable Variable { get; set; } = default!; + + /// + /// The value to assign. + /// + [Input] public Input Value { get; set; } = new(new Literal()); + /// protected override void Execute(ActivityExecutionContext context) { var value = context.Get(Value); @@ -45,10 +67,25 @@ public class SetVariable : Activity } } -[Activity("Elsa", "Primitives", "Set a workflow variable to a given value.")] +/// +/// Assign a workflow variable a value. +/// +[Activity("Elsa", "Primitives", "Assign a workflow variable a value.")] public class SetVariable : Activity { + /// + public SetVariable([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + + /// + /// The variable to assign the value to. + /// [Input] public Variable Variable { get; set; } = default!; + + /// + /// The value to assign. + /// [Input] public Input Value { get; set; } = new(default(object)); protected override void Execute(ActivityExecutionContext context) diff --git a/src/modules/Elsa.Workflows.Core/Activities/Switch.cs b/src/modules/Elsa.Workflows.Core/Activities/Switch.cs index 8b14fd7a4..8986a37e7 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Switch.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Switch.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions; using Elsa.Expressions.Models; @@ -15,6 +16,11 @@ namespace Elsa.Workflows.Core.Activities; [Activity("Elsa", "Control Flow", "Evaluate a set of case conditions and schedule the activity for a matching case.")] public class Switch : ActivityBase { + /// + public Switch([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) + { + } + /// /// The value to switch on. /// @@ -29,6 +35,7 @@ public class Switch : ActivityBase [Input(UIHint = "switch-editor")] public ICollection Cases { get; set; } = new List(); public IActivity? Default { get; set; } + /// protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) { context.Set(Output, Expression); diff --git a/src/modules/Elsa.Workflows.Core/Activities/While.cs b/src/modules/Elsa.Workflows.Core/Activities/While.cs index a3d78ba14..0c9956aa8 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/While.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/While.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; @@ -16,46 +17,52 @@ public class While : Activity }; [JsonConstructor] - public While(IActivity? body = default) + public While(IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { Body = body!; Behaviors.Add(this); Behaviors.Remove(); } - public While(Input condition, IActivity? body = default) : this(body) + public While(Input condition, IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(body, source, line) { Condition = condition; } - public While(Func> condition, IActivity? body = default) : this(new Input(condition), body) + public While(Func> condition, IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(condition), body, source, line) { } - public While(Func condition, IActivity? body = default) : this(new Input(condition), body) + public While(Func condition, IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(condition), body, source, line) { } - public While(Func> condition, IActivity? body = default) : this(new Input(condition), body) + public While(Func> condition, IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(condition), body, source, line) { } - public While(Func condition, IActivity? body = default) : this(new Input(condition), body) + public While(Func condition, IActivity? body = default, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new Input(condition), body, source, line) { } + /// + /// The condition to evaluate. + /// [Input(AutoEvaluate = false)] public Input Condition { get; set; } = new(false); + + /// + /// The to execute on every iteration. + /// [Port] public IActivity Body { get; set; } - protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) - { - await HandleIterationAsync(context); - } + /// + protected override async ValueTask ExecuteAsync(ActivityExecutionContext context) => await HandleIterationAsync(context); - private async ValueTask OnBodyCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) - { - await HandleIterationAsync(context); - } + private async ValueTask OnBodyCompleted(ActivityExecutionContext context, ActivityExecutionContext childContext) => await HandleIterationAsync(context); private async ValueTask HandleIterationAsync(ActivityExecutionContext context) { diff --git a/src/modules/Elsa.Workflows.Core/Activities/WriteLine.cs b/src/modules/Elsa.Workflows.Core/Activities/WriteLine.cs index 8828daa76..19624fe5b 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/WriteLine.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/WriteLine.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Runtime.CompilerServices; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Attributes; using Elsa.Workflows.Core.Implementations; @@ -7,33 +8,53 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Activities; +/// +/// +/// [Activity("Elsa", "Console", "Write a line of text to the console.")] public class WriteLine : Activity { - public WriteLine() - { - } - - public WriteLine(string text) : this(new Literal(text)) - { - } - - public WriteLine(Func text) : this(new DelegateBlockReference(text)) - { - } - - public WriteLine(Func text) : this(new DelegateBlockReference(text)) + /// + private WriteLine([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line) { } - public WriteLine(Variable variable) => Text = new Input(variable); - public WriteLine(Literal literal) => Text = new Input(literal); - public WriteLine(DelegateBlockReference delegateBlockExpression) => Text = new Input(delegateBlockExpression); - public WriteLine(Input text) => Text = text; + /// + public WriteLine(string text, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Literal(text), source, line) + { + } + + /// + public WriteLine(Func text, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new DelegateBlockReference(text), source, line) + { + } + + /// + public WriteLine(Func text, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) + : this(new DelegateBlockReference(text), source, line) + { + } + + /// + public WriteLine(Variable variable, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => Text = new Input(variable); + + /// + public WriteLine(Literal literal, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => Text = new Input(literal); + + /// + public WriteLine(DelegateBlockReference delegateBlockExpression, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => Text = new Input(delegateBlockExpression); + + /// + public WriteLine(Input text, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line) => Text = text; + /// + /// The text to write. + /// [Description("The text to write.")] public Input Text { get; set; } = default!; + /// protected override void Execute(ActivityExecutionContext context) { var text = context.Get(Text); diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index fb68cafa9..4c3a84e68 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -44,6 +44,10 @@ public static class ActivityExecutionContextExtensions var parentActivityInstanceId = context.ParentActivityExecutionContext?.Id; var workflowExecutionContext = context.WorkflowExecutionContext; var now = context.GetRequiredService().UtcNow; + + if (source == null && activity.Source != null) + source = $"{Path.GetFileName(activity.Source)}:{activity.Line}"; + var logEntry = new WorkflowExecutionLogEntry(activityInstanceId, parentActivityInstanceId, activity.Id, activity.Type, now, eventName, message, source, payload); workflowExecutionContext.ExecutionLog.Add(logEntry); return logEntry; diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs index fc370cb54..87a864615 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ExpressionExecutionContextExtensions.cs @@ -1,3 +1,4 @@ +using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Models; @@ -29,11 +30,11 @@ public static class ExpressionExecutionContextExtensions public static ActivityExecutionContext GetActivityExecutionContext(this ExpressionExecutionContext context) => (ActivityExecutionContext)context.TransientProperties[ActivityExecutionContextKey]; public static IDictionary GetInput(this ExpressionExecutionContext context) => (IDictionary)context.TransientProperties[InputKey]; - public static T? Get(this ExpressionExecutionContext context, Input? input) => input != null ? (T?)context.GetBlock(input.MemoryBlockReference).Value : default; - public static T? Get(this ExpressionExecutionContext context, Output output) => (T?)context.GetBlock(output.MemoryBlockReference).Value; + public static T? Get(this ExpressionExecutionContext context, Input? input) => input != null ? context.GetBlock(input.MemoryBlockReference).Value.ConvertTo() : default; + public static T? Get(this ExpressionExecutionContext context, Output output) => context.GetBlock(output.MemoryBlockReference).Value.ConvertTo(); public static object? Get(this ExpressionExecutionContext context, Output output) => context.GetBlock(output.MemoryBlockReference).Value; public static T? GetVariable(this ExpressionExecutionContext context, string name) => (T?)context.GetVariable(name); - public static T? GetVariable(this ExpressionExecutionContext context) => (T?)context.GetVariable(typeof(T).Name); + public static T? GetVariable(this ExpressionExecutionContext context) => context.GetVariable(typeof(T).Name).ConvertTo(); public static object? GetVariable(this ExpressionExecutionContext context, string name) => new Variable(name).Get(context); public static Variable SetVariable(this ExpressionExecutionContext context, T? value) => context.SetVariable(typeof(T).Name, value); public static Variable SetVariable(this ExpressionExecutionContext context, string name, T? value) => context.SetVariable(name, (object?)value); @@ -63,7 +64,7 @@ public static class ExpressionExecutionContextExtensions foreach (var l in currentRegister.Blocks) { if (!memoryBlocks.ContainsKey(l.Key)) - memoryBlocks.Add(l.Key, l.Value!.Value); + memoryBlocks.Add(l.Key, l.Value!.Value!); } currentRegister = currentRegister.Parent; diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index 23c5736b7..682d70ba4 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -45,7 +45,11 @@ public static class WorkflowExecutionContextExtensions public static void ScheduleBookmark(this WorkflowExecutionContext workflowExecutionContext, Bookmark bookmark) { // Construct bookmark. - var bookmarkedActivityContext = workflowExecutionContext.ActivityExecutionContexts.First(x => x.Id == bookmark.ActivityInstanceId); + var bookmarkedActivityContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == bookmark.ActivityInstanceId); + + if(bookmarkedActivityContext == null) + return; + var bookmarkedActivity = bookmarkedActivityContext.Activity; // Schedule the activity to resume. @@ -56,8 +60,8 @@ public static class WorkflowExecutionContextExtensions // If no resumption point was specified, use "Complete" to prevent the regular "ExecuteAsync" method to be invoked and instead complete the activity. workflowExecutionContext.ExecuteDelegate = bookmark.CallbackMethodName != null ? bookmarkedActivity.GetResumeActivityDelegate(bookmark.CallbackMethodName) : WorkflowExecutionContext.Complete; - // Remove the bookmark. - workflowExecutionContext.Bookmarks.Remove(bookmark); + // Store the bookmark to resume in the context. + workflowExecutionContext.ResumedBookmarkContext = new ResumedBookmarkContext(bookmark); } /// diff --git a/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs b/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs index b303ffb3d..5300ac4bb 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/BookmarkPayloadSerializer.cs @@ -17,5 +17,6 @@ public class BookmarkPayloadSerializer : IBookmarkPayloadSerializer } public T Deserialize(string json) where T : notnull => JsonSerializer.Deserialize(json, _settings)!; + public object Deserialize(string json, Type type) => JsonSerializer.Deserialize(json, type, _settings)!; public string Serialize(T payload) where T : notnull => JsonSerializer.Serialize(payload, payload.GetType(), _settings); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs b/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs index ee9fdd944..e3da5f25b 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/DefaultWorkflowExecutionContextFactory.cs @@ -26,7 +26,8 @@ public class DefaultWorkflowExecutionContextFactory : IWorkflowExecutionContextF _serviceProvider = serviceProvider; } - public async Task CreateAsync(Workflow workflow, + public async Task CreateAsync( + Workflow workflow, string instanceId, WorkflowState? workflowState, IDictionary? input = default, @@ -55,9 +56,9 @@ public class DefaultWorkflowExecutionContextFactory : IWorkflowExecutionContextF graph, scheduler, input, - executeActivityDelegate, + executeActivityDelegate, triggerActivityId, - default, + default, cancellationToken); // Restore workflow execution context from state, if provided. diff --git a/src/modules/Elsa.Workflows.Core/Implementations/IdentityGraphService.cs b/src/modules/Elsa.Workflows.Core/Implementations/IdentityGraphService.cs index 02a559577..cabd5cf4d 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/IdentityGraphService.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/IdentityGraphService.cs @@ -33,22 +33,17 @@ public class IdentityGraphService : IIdentityGraphService } } - private void AssignInputOutputs(IActivity activity) + public void AssignInputOutputs(IActivity activity) { var inputs = activity.GetInputs(); - - var assignedInputs = inputs.Where(x => - { - var memoryBlockReference = x.MemoryBlockReference(); - return memoryBlockReference.Id == null!; - }).ToList(); - var seed = 0; - foreach (var input in assignedInputs) + foreach (var input in inputs) { var blockReference = input.MemoryBlockReference(); - blockReference.Id = $"{activity.Id}:input-{++seed}"; + + if(string.IsNullOrEmpty(blockReference.Id)) + blockReference.Id = $"{activity.Id}:input-{++seed}"; } seed = 0; @@ -62,12 +57,14 @@ public class IdentityGraphService : IIdentityGraphService foreach (var output in assignedOutputs) { - var memoryReference = output.Value.MemoryBlockReference(); - memoryReference.Id = $"{activity.Id}:output-{++seed}"; + var blockReference = output.Value.MemoryBlockReference(); + + if(string.IsNullOrEmpty(blockReference.Id)) + blockReference.Id = $"{activity.Id}:output-{++seed}"; } } - private void AssignVariables(IActivity activity) + public void AssignVariables(IActivity activity) { var variables = activity.GetVariables(); var seed = 0; diff --git a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowRunner.cs index bbee99bf1..f1acef485 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowRunner.cs @@ -84,7 +84,7 @@ public class WorkflowRunner : IWorkflowRunner if (bookmarkId != null) { // Schedule the bookmark. - var bookmark = workflowExecutionContext.Bookmarks.FirstOrDefault(x => x.Id == bookmarkId); + var bookmark = workflowState.Bookmarks.FirstOrDefault(x => x.Id == bookmarkId); if (bookmark != null) workflowExecutionContext.ScheduleBookmark(bookmark); diff --git a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs index 5829870b8..d2cd48e8c 100644 --- a/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Implementations/WorkflowStateSerializer.cs @@ -105,6 +105,15 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer private void SerializeCompletionCallbacks(WorkflowState state, WorkflowExecutionContext workflowExecutionContext) { + // Assert all referenced owner contexts exist. + foreach (var completionCallback in workflowExecutionContext.CompletionCallbacks) + { + var owmnerContext = workflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x == completionCallback.Owner); + + if (owmnerContext == null) + throw new Exception("Lost an owner context"); + } + var completionCallbacks = workflowExecutionContext.CompletionCallbacks.Select(x => new CompletionCallbackState(x.Owner.Id, x.Child.Id, x.CompletionCallback?.Method.Name)); state.CompletionCallbacks = completionCallbacks.ToList(); } @@ -120,8 +129,9 @@ public class WorkflowStateSerializer : IWorkflowStateSerializer if (parentId != null) { var parentContext = activityExecutionContext.WorkflowExecutionContext.ActivityExecutionContexts.FirstOrDefault(x => x.Id == parentId); - - Debug.Assert(parentContext != null); + + if (parentContext == null) + throw new Exception("We lost a context"); } var activityExecutionContextState = new ActivityExecutionContextState diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index 53922f5b2..08afc0d76 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -36,6 +36,12 @@ public class DefaultActivityInvokerMiddleware : IActivityExecutionMiddleware // Reset execute delegate. workflowExecutionContext.ExecuteDelegate = null; + // If a bookmark was used to resume, burn it if not burnt by the activity. + var resumedBookmark = workflowExecutionContext.ResumedBookmarkContext?.Bookmark; + + if (resumedBookmark is { AutoBurn: true }) + workflowExecutionContext.Bookmarks.Remove(resumedBookmark); + // Update execution count. context.IncrementExecutionCount(); diff --git a/src/modules/Elsa.Workflows.Core/Models/Activity.cs b/src/modules/Elsa.Workflows.Core/Models/Activity.cs index 379d8e2f5..95a1c6fb7 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Activity.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Activity.cs @@ -3,58 +3,80 @@ using Elsa.Workflows.Core.Behaviors; namespace Elsa.Workflows.Core.Models; +/// +/// Base class for custom activities with auto-complete behavior. +/// public abstract class Activity : ActivityBase { - protected Activity() + /// + protected Activity(string? source = default, int? line = default) : base(source, line) { Behaviors.Add(this); } - protected Activity(string activityType) : this() + /// + protected Activity(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { Type = activityType; } } +/// +/// Base class for custom activities with auto-complete behavior that return a result. +/// public abstract class ActivityWithResult : Activity { - protected ActivityWithResult() + /// + protected ActivityWithResult(string? source = default, int? line = default) : base(source, line) { } - protected ActivityWithResult(string activityType) : base(activityType) + /// + protected ActivityWithResult(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { } - protected ActivityWithResult(MemoryBlockReference? output) + /// + protected ActivityWithResult(MemoryBlockReference? output, string? source = default, int? line = default) : base(source, line) { if (output != null) Result = new Output(output); } - protected ActivityWithResult(Output? output) + /// + protected ActivityWithResult(Output? output, string? source = default, int? line = default) : base(source, line) { Result = output; } + /// + /// The result of the activity. + /// public Output? Result { get; set; } } +/// +/// Base class for custom activities that return a result. +/// public abstract class ActivityBaseWithResult : ActivityBase { - protected ActivityBaseWithResult() + /// + protected ActivityBaseWithResult(string? source = default, int? line = default) : base(source, line) { } - protected ActivityBaseWithResult(string activityType) : base(activityType) + /// + protected ActivityBaseWithResult(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { } - protected ActivityBaseWithResult(MemoryBlockReference? output) + /// + protected ActivityBaseWithResult(MemoryBlockReference? output, string? source = default, int? line = default) : this(source, line) { if (output != null) Result = new Output(output); } - protected ActivityBaseWithResult(Output? output) + /// + protected ActivityBaseWithResult(Output? output, string? source = default, int? line = default) : this(source, line) { Result = output; } @@ -62,48 +84,68 @@ public abstract class ActivityBaseWithResult : ActivityBase public Output? Result { get; set; } } +/// +/// Base class for custom activities with auto-complete behavior that return a result. +/// public abstract class Activity : Activity { - protected Activity() + /// + protected Activity(string? source = default, int? line = default) : base(source, line) + { + } + + /// + protected Activity(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { } - protected Activity(string activityType) : base(activityType) - { - } - - protected Activity(MemoryBlockReference? output) + /// + protected Activity(MemoryBlockReference? output, string? source = default, int? line = default) : this(source, line) { if (output != null) Result = new Output(output); } - protected Activity(Output? output) + /// + protected Activity(Output? output, string? source = default, int? line = default) : this(source, line) { Result = output; } - + + /// + /// The result of the activity. + /// public Output? Result { get; set; } } +/// +/// Base class for custom activities that return a result. +/// public abstract class ActivityBase : ActivityBase { - protected ActivityBase() + /// + protected ActivityBase(string? source = default, int? line = default) : base(source, line) { } - protected ActivityBase(string activityType) : base(activityType) + /// + protected ActivityBase(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { } - protected ActivityBase(MemoryBlockReference? output) + /// + protected ActivityBase(MemoryBlockReference? output, string? source = default, int? line = default) : this(source, line) { if (output != null) Result = new Output(output); } - protected ActivityBase(Output? output) + /// + protected ActivityBase(Output? output, string? source = default, int? line = default) : this(source, line) { Result = output; } - + + /// + /// The result of the activity. + /// public Output? Result { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs index ac15bfd58..4d454f262 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityBase.cs @@ -6,31 +6,81 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Models; +/// +/// Base class for custom activities. +/// [DebuggerDisplay("{Type} - {Id}")] public abstract class ActivityBase : IActivity, ISignalHandler { private readonly ICollection _signalHandlers = new List(); - protected ActivityBase() + /// + /// Constructor. + /// + protected ActivityBase(string? source = default, int? line = default) { + Source = source; + Line = line; Type = ActivityTypeNameHelper.GenerateTypeName(GetType()); Version = 1; Behaviors.Add(this); Behaviors.Add(this); } - protected ActivityBase(string activityType, int version = 1) : this() + /// + protected ActivityBase(string activityType, int version = 1, string? source = default, int? line = default) : this(source, line) { Type = activityType; Version = version; } + /// + /// The unique ID of this activity within the . + /// public string Id { get; set; } = default!; + + /// + /// The technical type name. + /// public string Type { get; set; } + + /// + /// The version number. + /// public int Version { get; set; } + + /// + /// A flag indicating whether this activity can be used for starting a workflow. + /// Usually used for triggers, but also used to disambiguate between two or more starting activities and no starting activity was specified. + /// public bool CanStartWorkflow { get; set; } + + + /// + /// A flag indicating if this activity should execute synchronously or asynchronously. + /// By default, activities with an of , or + /// will execute synchronously, while activities of the kind will execute asynchronously. + /// public bool RunAsynchronously { get; set; } + + /// + /// A bag of properties that can be used by custom activities and other code such as middleware components to store additional values with the activity. + /// public IDictionary ApplicationProperties { get; set; } = new Dictionary(); + + /// + /// Automatically set to the current source file name when instantiating this activity inside of a workflow class or composite activity class. + /// + public string? Source { get; set; } + + /// + /// Automatically set to the current line of code when instantiating this activity inside of a workflow class or composite activity class. + /// + public int? Line { get; set; } + + /// + /// Stores metadata such as x and y coordinates when created via the designer. + /// public IDictionary Metadata { get; set; } = new Dictionary(); /// @@ -39,37 +89,51 @@ public abstract class ActivityBase : IActivity, ISignalHandler [JsonIgnore] public ICollection Behaviors { get; } = new List(); + /// + /// Override this method to implement activity-specific logic. + /// protected virtual ValueTask ExecuteAsync(ActivityExecutionContext context) { Execute(context); return ValueTask.CompletedTask; } + + /// + /// Override this method to implement activity-specific logic. + /// + protected virtual void Execute(ActivityExecutionContext context) + { + } + /// + /// Override this method to handle any signals sent from downstream activities. + /// protected virtual ValueTask OnSignalReceivedAsync(object signal, SignalContext context) { OnSignalReceived(signal, context); return ValueTask.CompletedTask; } + /// + /// Override this method to handle any signals sent from downstream activities. + /// protected virtual void OnSignalReceived(object signal, SignalContext context) { } - protected virtual void Execute(ActivityExecutionContext context) - { - } - /// - /// Notify the system that this activity completed. + /// Register a signal handler delegate. /// - protected async ValueTask CompleteAsync(ActivityExecutionContext context) - { - await context.CompleteActivityAsync(); - } - protected void OnSignalReceived(Type signalType, Func handler) => _signalHandlers.Add(new SignalHandlerRegistration(signalType, handler)); + + /// + /// Register a signal handler delegate. + /// protected void OnSignalReceived(Func handler) => OnSignalReceived(typeof(T), (signal, context) => handler((T)signal, context)); + /// + /// Register a signal handler delegate. + /// protected void OnSignalReceived(Action handler) { OnSignalReceived((signal, context) => @@ -78,6 +142,14 @@ public abstract class ActivityBase : IActivity, ISignalHandler return ValueTask.CompletedTask; }); } + + /// + /// Notify the workflow that this activity completed. + /// + protected async ValueTask CompleteAsync(ActivityExecutionContext context) + { + await context.CompleteActivityAsync(); + } async ValueTask IActivity.ExecuteAsync(ActivityExecutionContext context) { diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs index e9d98b252..4414bdb0a 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityExecutionContext.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Runtime.CompilerServices; using Elsa.Expressions.Helpers; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Services; @@ -83,6 +82,8 @@ public class ActivityExecutionContext // ReSharper disable once CollectionNeverQueried.Global public IDictionary JournalData { get; } = new Dictionary(); + public ResumedBookmarkContext? ResumedBookmarkContext => WorkflowExecutionContext.ResumedBookmarkContext; + public async ValueTask ScheduleActivityAsync(IActivity? activity, ActivityCompletionCallback? completionCallback = default, IEnumerable? references = default, object? tag = default) { await ScheduleActivityAsync(activity, this, completionCallback, references, tag); @@ -111,33 +112,39 @@ public class ActivityExecutionContext public void CreateBookmarks(IEnumerable payloads, ExecuteActivityDelegate? callback = default) { foreach (var payload in payloads) - CreateBookmark(payload, callback); + CreateBookmark(new CreateBookmarkOptions(payload, callback)); } public void AddBookmarks(IEnumerable bookmarks) => _bookmarks.AddRange(bookmarks); public void AddBookmark(Bookmark bookmark) => _bookmarks.Add(bookmark); - public Bookmark CreateBookmark(ExecuteActivityDelegate callback) => CreateBookmark(default, callback); + public Bookmark CreateBookmark(ExecuteActivityDelegate callback) => CreateBookmark(new CreateBookmarkOptions(default, callback)); + public Bookmark CreateBookmark(object payload, ExecuteActivityDelegate callback) => CreateBookmark(new CreateBookmarkOptions(payload, callback)); + public Bookmark CreateBookmark(object payload) => CreateBookmark(new CreateBookmarkOptions(payload)); /// /// Creates a bookmark so that this activity can be resumed at a later time. /// Creating a bookmark will automatically suspend the workflow after all pending activities have executed. /// - public Bookmark CreateBookmark(object? payload = default, ExecuteActivityDelegate? callback = default) + public Bookmark CreateBookmark(CreateBookmarkOptions? options = default) { + var payload = options?.Payload; + var callback = options?.Callback; + var activityTypeName = options?.ActivityTypeName ?? Activity.Type; var bookmarkHasher = GetRequiredService(); var identityGenerator = GetRequiredService(); var payloadSerializer = GetRequiredService(); var payloadJson = payload != null ? payloadSerializer.Serialize(payload) : default; - var hash = bookmarkHasher.Hash(Activity.Type, payloadJson); + var hash = bookmarkHasher.Hash(activityTypeName, payloadJson); var bookmark = new Bookmark( identityGenerator.GenerateId(), - Activity.Type, + activityTypeName, hash, payloadJson, Activity.Id, Id, + options?.AutoBurn ?? true, callback?.Method.Name); AddBookmark(bookmark); @@ -195,7 +202,7 @@ public class ActivityExecutionContext public object? Get(MemoryBlockReference blockReference) { - var location = GetBlock(blockReference) ?? throw new InvalidOperationException($"No location found with ID {blockReference.Id}. Did you forget to declare a variable with a container?"); + var location = GetMemoryBlock(blockReference) ?? throw new InvalidOperationException($"No location found with ID {blockReference.Id}. Did you forget to declare a variable with a container?"); return location.Value; } @@ -225,8 +232,8 @@ public class ActivityExecutionContext internal void IncrementExecutionCount() => _executionCount++; - private MemoryBlock? GetBlock(MemoryBlockReference locationBlockReference) => - ExpressionExecutionContext.Memory.TryGetBlock(locationBlockReference.Id, out var location) - ? location - : ParentActivityExecutionContext?.GetBlock(locationBlockReference); + private MemoryBlock? GetMemoryBlock(MemoryBlockReference locationBlockReference) => + ExpressionExecutionContext.Memory.TryGetBlock(locationBlockReference.Id, out var memoryBlock) + ? memoryBlock + : ParentActivityExecutionContext?.GetMemoryBlock(locationBlockReference); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs b/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs index e9c64f974..c776899c5 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Bookmark.cs @@ -9,6 +9,7 @@ public record Bookmark( string? Data, string ActivityId, string ActivityInstanceId, + bool AutoBurn = true, string? CallbackMethodName = default ) { diff --git a/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkOptions.cs b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkOptions.cs new file mode 100644 index 000000000..83a74fe9d --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/CreateBookmarkOptions.cs @@ -0,0 +1,5 @@ +using Elsa.Workflows.Core.Services; + +namespace Elsa.Workflows.Core.Models; + +public record CreateBookmarkOptions(object? Payload = default, ExecuteActivityDelegate? Callback = default, string? ActivityTypeName = default, bool AutoBurn = true); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/EventGenerator.cs b/src/modules/Elsa.Workflows.Core/Models/EventGenerator.cs index f2ab0d9ba..0d09d58ae 100644 --- a/src/modules/Elsa.Workflows.Core/Models/EventGenerator.cs +++ b/src/modules/Elsa.Workflows.Core/Models/EventGenerator.cs @@ -2,13 +2,18 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Models; +/// +/// Generates events on a workflow instance. +/// public abstract class EventGenerator : Trigger, IEventGenerator { - protected EventGenerator() + /// + protected EventGenerator(string? source = default, int? line = default) : base(source, line) { } - protected EventGenerator(string triggerType) : base(triggerType) + /// + protected EventGenerator(string triggerType, int version = 1, string? source = default, int? line = default) : base(triggerType, version, source, line) { } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/ResumedBookmarkContext.cs b/src/modules/Elsa.Workflows.Core/Models/ResumedBookmarkContext.cs new file mode 100644 index 000000000..06edad5d4 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Models/ResumedBookmarkContext.cs @@ -0,0 +1,3 @@ +namespace Elsa.Workflows.Core.Models; + +public record ResumedBookmarkContext(Bookmark Bookmark); \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/Trigger.cs b/src/modules/Elsa.Workflows.Core/Models/Trigger.cs index c3a138c73..f6fea5b3f 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Trigger.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Trigger.cs @@ -2,13 +2,18 @@ using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Core.Models; +/// +/// Represents an activity that acts as a workflow trigger. +/// public abstract class Trigger : ActivityBase, ITrigger { - protected Trigger() + /// + protected Trigger(string? source = default, int? line = default) : base(source, line) { } - protected Trigger(string activityType) : base(activityType) + /// + protected Trigger(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { } @@ -36,11 +41,11 @@ public abstract class Trigger : ActivityBase, ITrigger public abstract class Trigger : ActivityBase, ITrigger { - protected Trigger() + protected Trigger(string? source = default, int? line = default) : base(source, line) { } - protected Trigger(string activityType) : base(activityType) + protected Trigger(string activityType, int version = 1, string? source = default, int? line = default) : base(activityType, version, source, line) { } diff --git a/src/modules/Elsa.Workflows.Core/Models/Variable.cs b/src/modules/Elsa.Workflows.Core/Models/Variable.cs index d269b131a..167e890cf 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Variable.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Variable.cs @@ -7,7 +7,6 @@ public class Variable : MemoryBlockReference { public Variable() { - Id = Guid.NewGuid().ToString("N"); } public Variable(string name) diff --git a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs index 142aeca5a..6fbb3b3dd 100644 --- a/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Models/WorkflowExecutionContext.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Diagnostics; using Elsa.Common.Extensions; using Elsa.Expressions.Models; using Elsa.Workflows.Core.Services; @@ -74,6 +73,7 @@ public class WorkflowExecutionContext public IDictionary TransientProperties { get; set; } = new Dictionary(); public ExecuteActivityDelegate? ExecuteDelegate { get; set; } + public ResumedBookmarkContext? ResumedBookmarkContext { get; set; } public string? TriggerActivityId { get; set; } public CancellationToken CancellationToken { get; } public ICollection CompletionCallbacks => new ReadOnlyCollection(_completionCallbackEntries); @@ -166,7 +166,14 @@ public class WorkflowExecutionContext foreach (var childContext in childContexts) RemoveActivityExecutionContext(childContext); + // Remove the context. _activityExecutionContexts.Remove(context); + + // Remove all associated completion callbacks. + context.ClearCompletionCallbacks(); + + // Remove all associated bookmarks. + Bookmarks.RemoveWhere(x => x.ActivityInstanceId == context.Id); } public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context); diff --git a/src/modules/Elsa.Workflows.Core/Services/IActivity.cs b/src/modules/Elsa.Workflows.Core/Services/IActivity.cs index 9ce9c93f8..0d135b8bf 100644 --- a/src/modules/Elsa.Workflows.Core/Services/IActivity.cs +++ b/src/modules/Elsa.Workflows.Core/Services/IActivity.cs @@ -25,17 +25,27 @@ public interface IActivity /// /// A value indicating whether this activity can start instances of the workflow it is a part of. /// - public bool CanStartWorkflow { get; set; } + bool CanStartWorkflow { get; set; } /// /// A value indicating whether this activity can be executed asynchronously in the background. /// - public bool RunAsynchronously { get; set; } + bool RunAsynchronously { get; set; } /// /// Can contain application-specific information about this activity. /// IDictionary ApplicationProperties { get; set; } + + /// + /// The source file where this activity was instantiated, if any. + /// + string? Source { get; set; } + + /// + /// The source file line number where this activity was instantiated, if any. + /// + int? Line { get; set; } /// /// Invoked when the activity executes. diff --git a/src/modules/Elsa.Workflows.Core/Services/IBookmarkPayloadSerializer.cs b/src/modules/Elsa.Workflows.Core/Services/IBookmarkPayloadSerializer.cs index 560f6651c..ad883b07c 100644 --- a/src/modules/Elsa.Workflows.Core/Services/IBookmarkPayloadSerializer.cs +++ b/src/modules/Elsa.Workflows.Core/Services/IBookmarkPayloadSerializer.cs @@ -3,5 +3,6 @@ namespace Elsa.Workflows.Core.Services; public interface IBookmarkPayloadSerializer { T Deserialize(string json) where T : notnull; + object Deserialize(string json, Type type); string Serialize(T payload) where T : notnull; } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/IIdentityGraphService.cs b/src/modules/Elsa.Workflows.Core/Services/IIdentityGraphService.cs index ec5aadff9..6539e0d21 100644 --- a/src/modules/Elsa.Workflows.Core/Services/IIdentityGraphService.cs +++ b/src/modules/Elsa.Workflows.Core/Services/IIdentityGraphService.cs @@ -7,4 +7,6 @@ public interface IIdentityGraphService Task AssignIdentitiesAsync(Workflow workflow, CancellationToken cancellationToken = default); Task AssignIdentitiesAsync(IActivity root, CancellationToken cancellationToken = default); void AssignIdentities(ActivityNode root); + void AssignInputOutputs(IActivity activity); + void AssignVariables(IActivity activity); } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionSyntaxProvider.cs b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionSyntaxProvider.cs index 429f197e9..911f36392 100644 --- a/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionSyntaxProvider.cs +++ b/src/modules/Elsa.Workflows.Management/Providers/DefaultExpressionSyntaxProvider.cs @@ -53,15 +53,7 @@ public class DefaultExpressionSyntaxProvider : IExpressionSyntaxProvider Syntax = syntax, Type = typeof(TExpression), CreateExpression = constructor, - CreateBlockReference = context => - { - var reference = createBlockReference(context); - - if (string.IsNullOrWhiteSpace(reference.Id)) - reference.Id = context.MemoryReferenceId; - - return reference; - }, + CreateBlockReference = createBlockReference, CreateSerializableObject = context => new { Type = syntax, diff --git a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs index 2e3eead71..f2430895a 100644 --- a/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Management/Serialization/Converters/ActivityJsonConverter.cs @@ -15,12 +15,14 @@ public class ActivityJsonConverter : JsonConverter { private readonly IActivityRegistry _activityRegistry; private readonly IActivityFactory _activityFactory; + private readonly IIdentityGraphService _identityGraphService; private readonly IServiceProvider _serviceProvider; - public ActivityJsonConverter(IActivityRegistry activityRegistry, IActivityFactory activityFactory, IServiceProvider serviceProvider) + public ActivityJsonConverter(IActivityRegistry activityRegistry, IActivityFactory activityFactory, IIdentityGraphService identityGraphService, IServiceProvider serviceProvider) { _activityRegistry = activityRegistry; _activityFactory = activityFactory; + _identityGraphService = identityGraphService; _serviceProvider = serviceProvider; } @@ -56,6 +58,9 @@ public class ActivityJsonConverter : JsonConverter var context = new ActivityConstructorContext(doc.RootElement, newOptions); var activity = activityDescriptor.Constructor(context); + + _identityGraphService.AssignInputOutputs(activity); + _identityGraphService.AssignVariables(activity); return activity; } diff --git a/src/modules/Elsa.Workflows.Management/Serialization/Converters/InputJsonConverter.cs b/src/modules/Elsa.Workflows.Management/Serialization/Converters/InputJsonConverter.cs index 470135b7a..59d91df00 100644 --- a/src/modules/Elsa.Workflows.Management/Serialization/Converters/InputJsonConverter.cs +++ b/src/modules/Elsa.Workflows.Management/Serialization/Converters/InputJsonConverter.cs @@ -3,7 +3,6 @@ using System.Text.Json.Serialization; using Elsa.Expressions.Models; using Elsa.Expressions.Services; using Elsa.Workflows.Core.Models; -using Elsa.Workflows.Core.Services; namespace Elsa.Workflows.Management.Serialization.Converters; @@ -13,17 +12,17 @@ namespace Elsa.Workflows.Management.Serialization.Converters; public class InputJsonConverter : JsonConverter> { private readonly IExpressionSyntaxRegistry _expressionSyntaxRegistry; - private readonly IIdentityGenerator _identityGenerator; /// - public InputJsonConverter(IExpressionSyntaxRegistry expressionSyntaxRegistry, IIdentityGenerator identityGenerator) + public InputJsonConverter(IExpressionSyntaxRegistry expressionSyntaxRegistry) { _expressionSyntaxRegistry = expressionSyntaxRegistry; - _identityGenerator = identityGenerator; } + /// public override bool CanConvert(Type typeToConvert) => typeof(Input).IsAssignableFrom(typeToConvert); + /// public override Input Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (!JsonDocument.TryParseValue(ref reader, out var doc)) @@ -32,12 +31,14 @@ public class InputJsonConverter : JsonConverter> if (doc.RootElement.ValueKind != JsonValueKind.Object) return default!; - if (!doc.RootElement.TryGetProperty("typeName", out var inputTargetTypeElement)) + if (!doc.RootElement.TryGetProperty("typeName", out _)) return default!; - var memoryReferenceId = doc.RootElement.TryGetProperty("memoryReference", out var memoryReferenceElement) && memoryReferenceElement.ValueKind == JsonValueKind.String + var memoryReferenceId = doc.RootElement.TryGetProperty("memoryReference", out var memoryReferenceElement) && memoryReferenceElement.ValueKind == JsonValueKind.Object ? memoryReferenceElement.GetProperty("id").GetString()! - : _identityGenerator.GenerateId(); + : memoryReferenceElement.ValueKind == JsonValueKind.String + ? memoryReferenceElement.GetString()! + : throw new Exception("No input ID specified"); var expressionElement = doc.RootElement.GetProperty("expression"); @@ -52,11 +53,12 @@ public class InputJsonConverter : JsonConverter> var context = new ExpressionConstructorContext(expressionElement, options); var expression = expressionSyntaxDescriptor.CreateExpression(context); - var locationReference = expressionSyntaxDescriptor.CreateBlockReference(new BlockReferenceConstructorContext(expression, memoryReferenceId)); + var memoryBlockReference = expressionSyntaxDescriptor.CreateBlockReference(new BlockReferenceConstructorContext(expression, memoryReferenceId)); - return (Input)Activator.CreateInstance(typeof(Input), expression, locationReference)!; + return (Input)Activator.CreateInstance(typeof(Input), expression, memoryBlockReference)!; } + /// public override void Write(Utf8JsonWriter writer, Input value, JsonSerializerOptions options) { var expression = value.Expression; diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj index 6621afa9f..47202478b 100644 --- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj @@ -18,6 +18,7 @@ + diff --git a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs index ecbcbb5cf..7867cb6bc 100644 --- a/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs +++ b/src/modules/Elsa.Workflows.Runtime/Features/WorkflowRuntimeFeature.cs @@ -13,6 +13,8 @@ using Elsa.Workflows.Runtime.Implementations; using Elsa.Workflows.Runtime.Models; using Elsa.Workflows.Runtime.Options; using Elsa.Workflows.Runtime.Services; +using Medallion.Threading; +using Medallion.Threading.FileSystem; using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows.Runtime.Features; @@ -52,6 +54,8 @@ public class WorkflowRuntimeFeature : FeatureBase public Func WorkflowTriggerStore { get; set; } = sp => sp.GetRequiredService(); public Func WorkflowExecutionLogStore { get; set; } = sp => sp.GetRequiredService(); + public Func DistributedLockProvider { get; set; } = _ => + new FileDistributedSynchronizationProvider(new DirectoryInfo( Path.Combine(Environment.CurrentDirectory, "App_Data/locks"))); public Func WorkflowStateExporter { get; set; } = sp => sp.GetRequiredService(); @@ -83,11 +87,14 @@ public class WorkflowRuntimeFeature : FeatureBase .AddSingleton(WorkflowTriggerStore) .AddSingleton(WorkflowExecutionLogStore) - // Memory Stores + // Memory stores. .AddMemoryStore() .AddMemoryStore() .AddMemoryStore() .AddMemoryStore() + + // Distributed locking. + .AddSingleton(DistributedLockProvider) // Workflow definition providers. .AddWorkflowDefinitionProvider() diff --git a/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowRuntime.cs b/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowRuntime.cs index 89f5dbdeb..df23c3e81 100644 --- a/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowRuntime.cs +++ b/src/modules/Elsa.Workflows.Runtime/Implementations/DefaultWorkflowRuntime.cs @@ -4,6 +4,7 @@ using Elsa.Workflows.Core.Services; using Elsa.Workflows.Core.State; using Elsa.Workflows.Runtime.Models; using Elsa.Workflows.Runtime.Services; +using Medallion.Threading; namespace Elsa.Workflows.Runtime.Implementations; @@ -15,6 +16,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime private readonly ITriggerStore _triggerStore; private readonly IBookmarkStore _bookmarkStore; private readonly IBookmarkHasher _hasher; + private readonly IDistributedLockProvider _distributedLockProvider; public DefaultWorkflowRuntime( IWorkflowHostFactory workflowHostFactory, @@ -22,7 +24,8 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime IWorkflowStateStore workflowStateStore, ITriggerStore triggerStore, IBookmarkStore bookmarkStore, - IBookmarkHasher hasher) + IBookmarkHasher hasher, + IDistributedLockProvider distributedLockProvider) { _workflowHostFactory = workflowHostFactory; _workflowDefinitionService = workflowDefinitionService; @@ -30,6 +33,7 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime _triggerStore = triggerStore; _bookmarkStore = bookmarkStore; _hasher = hasher; + _distributedLockProvider = distributedLockProvider; } public async Task StartWorkflowAsync(string definitionId, StartWorkflowRuntimeOptions options, CancellationToken cancellationToken = default) @@ -56,32 +60,35 @@ public class DefaultWorkflowRuntime : IWorkflowRuntime public async Task ResumeWorkflowAsync(string workflowInstanceId, ResumeWorkflowRuntimeOptions options, CancellationToken cancellationToken = default) { - var workflowState = await _workflowStateStore.LoadAsync(workflowInstanceId, cancellationToken); + await using (await _distributedLockProvider.AcquireLockAsync(workflowInstanceId, TimeSpan.FromMinutes(1), cancellationToken)) + { + var workflowState = await _workflowStateStore.LoadAsync(workflowInstanceId, cancellationToken); - if (workflowState == null) - throw new Exception($"Workflow instance {workflowInstanceId} not found"); + if (workflowState == null) + throw new Exception($"Workflow instance {workflowInstanceId} not found"); - var definitionId = workflowState.DefinitionId; - var version = workflowState.DefinitionVersion; + var definitionId = workflowState.DefinitionId; + var version = workflowState.DefinitionVersion; - var workflowDefinition = await _workflowDefinitionService.FindAsync( - definitionId, - VersionOptions.SpecificVersion(version), - cancellationToken); + var workflowDefinition = await _workflowDefinitionService.FindAsync( + definitionId, + VersionOptions.SpecificVersion(version), + cancellationToken); - if (workflowDefinition == null) - throw new Exception("Specified workflow definition and version does not exist"); - - var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken); - var workflowHost = await _workflowHostFactory.CreateAsync(workflow, workflowState, cancellationToken); - var resumeWorkflowOptions = new ResumeWorkflowHostOptions(options.CorrelationId, options.BookmarkId, options.ActivityId, options.Input); + if (workflowDefinition == null) + throw new Exception("Specified workflow definition and version does not exist"); - await workflowHost.ResumeWorkflowAsync(resumeWorkflowOptions, cancellationToken); - workflowState = workflowHost.WorkflowState; + var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken); + var workflowHost = await _workflowHostFactory.CreateAsync(workflow, workflowState, cancellationToken); + var resumeWorkflowOptions = new ResumeWorkflowHostOptions(options.CorrelationId, options.BookmarkId, options.ActivityId, options.Input); - await SaveWorkflowStateAsync(workflowState, cancellationToken); + await workflowHost.ResumeWorkflowAsync(resumeWorkflowOptions, cancellationToken); + workflowState = workflowHost.WorkflowState; - return new ResumeWorkflowResult(workflowState.Bookmarks); + await SaveWorkflowStateAsync(workflowState, cancellationToken); + + return new ResumeWorkflowResult(workflowState.Bookmarks); + } } public async Task> ResumeWorkflowsAsync(string activityTypeName, object bookmarkPayload, ResumeWorkflowRuntimeOptions options, CancellationToken cancellationToken = default) diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/PersistBookmarkMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/PersistBookmarkMiddleware.cs index 115bfdc8b..d67775fd1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/PersistBookmarkMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/PersistBookmarkMiddleware.cs @@ -46,9 +46,9 @@ public class PersistBookmarkMiddleware : WorkflowExecutionMiddleware await _eventPublisher.PublishAsync(new WorkflowBookmarksIndexed(new IndexedWorkflowBookmarks(context.Id, diff.Added, diff.Removed)), cancellationToken); // Notify all interested activities that the bookmarks have been persisted. - var activityExecutionContexts = context.ActivityExecutionContexts.Where(x => x.Activity is IBookmarksPersistedHandler).ToList(); + var activityExecutionContexts = context.ActivityExecutionContexts.Where(x => x.Activity is IBookmarksPersistedHandler && x.Bookmarks.Any()).ToList(); - foreach (var activityExecutionContext in activityExecutionContexts) + foreach (var activityExecutionContext in activityExecutionContexts) await ((IBookmarksPersistedHandler)activityExecutionContext.Activity).BookmarksPersistedAsync(activityExecutionContext); } } \ No newline at end of file