From d060d3acb827b96116d7222ba154ca2a0bbf16e9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 23 Jan 2026 19:05:55 +0100 Subject: [PATCH 1/8] Add `GetExecutionChainAsync` method to Dapper and MongoDB activity execution record stores Implemented a method to retrieve the execution chain of activity records with optional cross-workflow traversal and pagination capabilities. Expanded filtering and mapping functionality to support new attributes. --- .../DapperActivityExecutionRecordStore.cs | 75 ++++++++++++++++++- .../Runtime/ActivityExecutionLogStore.cs | 44 ++++++++++- 2 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs index 5de5fafc..dc50ede5 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Models; using Elsa.Persistence.Dapper.Extensions; using Elsa.Persistence.Dapper.Models; using Elsa.Persistence.Dapper.Modules.Runtime.Records; @@ -88,6 +89,49 @@ public async Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Ca return await store.DeleteAsync(q => ApplyFilter(q, filter), cancellationToken); } + public async Task> GetExecutionChainAsync( + string activityExecutionId, + bool includeCrossWorkflowChain = true, + int? skip = null, + int? take = null, + CancellationToken cancellationToken = default) + { + var chain = new List(); + var currentId = activityExecutionId; + + // Traverse the chain backwards from the specified record to the root. + while (currentId != null) + { + var id = currentId; + var record = await store.FindAsync(q => q.Is(nameof(ActivityExecutionRecordRecord.Id), id), cancellationToken); + + if (record == null) + break; + + var mappedRecord = Map(record); + chain.Add(mappedRecord); + + // If not including cross-workflow chain and we hit a workflow boundary, stop. + if (!includeCrossWorkflowChain && mappedRecord.SchedulingWorkflowInstanceId != null) + break; + + currentId = mappedRecord.SchedulingActivityExecutionId; + } + + // Reverse to get root-to-leaf order. + chain.Reverse(); + + var totalCount = chain.Count; + + // Apply pagination if specified. + if (skip.HasValue) + chain = chain.Skip(skip.Value).ToList(); + if (take.HasValue) + chain = chain.Take(take.Value).ToList(); + + return Page.Of(chain, totalCount); + } + private static void ApplyFilter(ParameterizedQuery query, ActivityExecutionRecordFilter filter) { query @@ -95,8 +139,21 @@ private static void ApplyFilter(ParameterizedQuery query, ActivityExecutionRecor .In(nameof(ActivityExecutionRecordRecord.Id), filter.Ids) .Is(nameof(ActivityExecutionRecordRecord.ActivityId), filter.ActivityId) .In(nameof(ActivityExecutionRecordRecord.ActivityId), filter.ActivityIds) + .Is(nameof(ActivityExecutionRecordRecord.ActivityNodeId), filter.ActivityNodeId) + .In(nameof(ActivityExecutionRecordRecord.ActivityNodeId), filter.ActivityNodeIds) + .Is(nameof(ActivityExecutionRecordRecord.ActivityName), filter.Name) + .In(nameof(ActivityExecutionRecordRecord.ActivityName), filter.Names) + .Is(nameof(ActivityExecutionRecordRecord.Status), filter.Status?.ToString()) + .In(nameof(ActivityExecutionRecordRecord.Status), filter.Statuses?.Select(x => x.ToString())) .Is(nameof(ActivityExecutionRecordRecord.WorkflowInstanceId), filter.WorkflowInstanceId) - .In(nameof(ActivityExecutionRecordRecord.WorkflowInstanceId), filter.WorkflowInstanceIds); + .In(nameof(ActivityExecutionRecordRecord.WorkflowInstanceId), filter.WorkflowInstanceIds) + .Is(nameof(ActivityExecutionRecordRecord.SchedulingActivityExecutionId), filter.SchedulingActivityExecutionId) + .In(nameof(ActivityExecutionRecordRecord.SchedulingActivityExecutionId), filter.SchedulingActivityExecutionIds) + .Is(nameof(ActivityExecutionRecordRecord.SchedulingActivityId), filter.SchedulingActivityId) + .In(nameof(ActivityExecutionRecordRecord.SchedulingActivityId), filter.SchedulingActivityIds) + .Is(nameof(ActivityExecutionRecordRecord.SchedulingWorkflowInstanceId), filter.SchedulingWorkflowInstanceId) + .In(nameof(ActivityExecutionRecordRecord.SchedulingWorkflowInstanceId), filter.SchedulingWorkflowInstanceIds) + .Is(nameof(ActivityExecutionRecordRecord.CallStackDepth), filter.CallStackDepth); if (filter.Completed != null) { @@ -127,6 +184,12 @@ private ActivityExecutionRecordRecord Map(ActivityExecutionRecord source) SerializedOutputs = source.Outputs?.Any() == true ? safeSerializer.Serialize(source.Outputs) : null, SerializedException = source.Exception != null ? payloadSerializer.Serialize(source.Exception) : null, SerializedProperties = source.Properties?.Any() == true ? safeSerializer.Serialize(source.Properties) : null, + SerializedMetadata = source.Metadata?.Any() == true ? payloadSerializer.Serialize(source.Metadata) : null, + AggregateFaultCount = source.AggregateFaultCount, + SchedulingActivityExecutionId = source.SchedulingActivityExecutionId, + SchedulingActivityId = source.SchedulingActivityId, + SchedulingWorkflowInstanceId = source.SchedulingWorkflowInstanceId, + CallStackDepth = source.CallStackDepth, TenantId = source.TenantId }; } @@ -151,6 +214,12 @@ private ActivityExecutionRecord Map(ActivityExecutionRecordRecord source) Outputs = source.SerializedOutputs != null ? safeSerializer.Deserialize>(source.SerializedOutputs) : null, Exception = source.SerializedException != null ? payloadSerializer.Deserialize(source.SerializedException) : null, Properties = source.SerializedProperties != null ? safeSerializer.Deserialize>(source.SerializedProperties) : null, + Metadata = source.SerializedMetadata != null ? payloadSerializer.Deserialize>(source.SerializedMetadata) : null, + AggregateFaultCount = source.AggregateFaultCount, + SchedulingActivityExecutionId = source.SchedulingActivityExecutionId, + SchedulingActivityId = source.SchedulingActivityId, + SchedulingWorkflowInstanceId = source.SchedulingWorkflowInstanceId, + CallStackDepth = source.CallStackDepth, TenantId = source.TenantId }; } @@ -170,7 +239,9 @@ private ActivityExecutionRecordSummary MapSummary(ActivityExecutionSummaryRecord HasBookmarks = source.HasBookmarks, Status = Enum.Parse(source.Status), ActivityTypeVersion = source.ActivityTypeVersion, + AggregateFaultCount = source.AggregateFaultCount, + Metadata = source.SerializedMetadata != null ? payloadSerializer.Deserialize>(source.SerializedMetadata) : null, TenantId = source.TenantId }; } -} \ No newline at end of file +} diff --git a/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs b/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs index 751af597..f39b334c 100644 --- a/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs +++ b/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs @@ -75,6 +75,48 @@ public Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Cancella return mongoDbStore.DeleteWhereAsync(queryable => Filter(queryable, filter), x => x.Id, cancellationToken); } + public async Task> GetExecutionChainAsync( + string activityExecutionId, + bool includeCrossWorkflowChain = true, + int? skip = null, + int? take = null, + CancellationToken cancellationToken = default) + { + var chain = new List(); + var currentId = activityExecutionId; + + // Traverse the chain backwards from the specified record to the root. + while (currentId != null) + { + var id = currentId; + var record = await mongoDbStore.FindAsync(queryable => queryable.Where(x => x.Id == id), cancellationToken); + + if (record == null) + break; + + chain.Add(record); + + // If not including cross-workflow chain and we hit a workflow boundary, stop. + if (!includeCrossWorkflowChain && record.SchedulingWorkflowInstanceId != null) + break; + + currentId = record.SchedulingActivityExecutionId; + } + + // Reverse to get root-to-leaf order. + chain.Reverse(); + + var totalCount = chain.Count; + + // Apply pagination if specified. + if (skip.HasValue) + chain = chain.Skip(skip.Value).ToList(); + if (take.HasValue) + chain = chain.Take(take.Value).ToList(); + + return Page.Of(chain, totalCount); + } + private IQueryable Filter(IQueryable queryable, ActivityExecutionRecordFilter filter) { return filter.Apply(queryable); @@ -89,4 +131,4 @@ private IQueryable Paginate(IQueryable Date: Fri, 23 Jan 2026 19:06:07 +0100 Subject: [PATCH 2/8] Add additional fields to activity execution and summary records Introduced properties for metadata, fault aggregation, scheduling, and call stack depth to enhance activity execution tracking and analysis. --- .../Records/ActivityExecutionRecord.cs | 34 +++++++++++++++++-- .../Records/ActivityExecutionSummaryRecord.cs | 12 ++++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionRecord.cs b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionRecord.cs index 2b171b04..f9a9f413 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionRecord.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionRecord.cs @@ -62,6 +62,11 @@ internal class ActivityExecutionRecordRecord : Record /// public string? SerializedProperties { get; set; } + /// + /// Lightweight metadata associated with the activity execution. + /// + public string? SerializedMetadata { get; set; } + /// /// Gets or sets the time at which the activity execution began. /// @@ -76,9 +81,34 @@ internal class ActivityExecutionRecordRecord : Record /// Gets or sets the status of the activity. /// public string Status { get; set; } = null!; - + + /// + /// Gets or sets the aggregated count of faults encountered during the execution of the activity instance and its descendants. + /// + public int AggregateFaultCount { get; set; } + /// /// Gets or sets the time at which the activity execution completed. /// public DateTimeOffset? CompletedAt { get; set; } -} \ No newline at end of file + + /// + /// The ID of the activity execution context that scheduled this activity execution. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The ID of the activity that scheduled this activity execution (denormalized). + /// + public string? SchedulingActivityId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this activity execution. + /// + public string? SchedulingWorkflowInstanceId { get; set; } + + /// + /// The depth of this activity in the call stack (0 for root activities). + /// + public int? CallStackDepth { get; set; } +} diff --git a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionSummaryRecord.cs b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionSummaryRecord.cs index b0375695..834fb87c 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionSummaryRecord.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Records/ActivityExecutionSummaryRecord.cs @@ -51,9 +51,19 @@ internal class ActivityExecutionSummaryRecord : Record /// Gets or sets the status of the activity. /// public string Status { get; set; } = null!; + + /// + /// Lightweight metadata associated with the activity execution. + /// + public string? SerializedMetadata { get; set; } + + /// + /// Gets or sets the aggregated count of faults encountered during the execution of the activity instance and its descendants. + /// + public int AggregateFaultCount { get; set; } /// /// Gets or sets the time at which the activity execution completed. /// public DateTimeOffset? CompletedAt { get; set; } -} \ No newline at end of file +} From b626fd73e826d6d6bbe5e9e533c44d6dce0f0705 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 23 Jan 2026 19:06:26 +0100 Subject: [PATCH 3/8] Update activity provider constructors to return `CreateActivity` results Refactored constructors across multiple activity providers to return the full result object from `CreateActivity` method, ensuring consistent object handling. --- .../ActivityProviders/AgentActivityProvider.cs | 5 +++-- .../OrchardContentItemsEventActivityProvider.cs | 5 +++-- .../ActivityProviders/WebhookEventActivityProvider.cs | 5 +++-- .../Services/MassTransitActivityTypeProvider.cs | 10 ++++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs index 4d4d8972..b765c09f 100644 --- a/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs +++ b/src/modules/agents/Elsa.Agents.Activities/ActivityProviders/AgentActivityProvider.cs @@ -52,11 +52,12 @@ private async Task CreateAgentActivityDescriptor(AgentConfig activityDescriptor.Constructor = context => { - var activity = context.CreateActivity(); + var result = context.CreateActivity(); + var activity = result.Activity; activity.Type = activityTypeName; activity.AgentName = agentConfig.Name; activity.RunAsynchronously = true; - return activity; + return result; }; activityDescriptor.Inputs.Clear(); diff --git a/src/modules/cms/Elsa.OrchardCore/ActivityProviders/OrchardContentItemsEventActivityProvider.cs b/src/modules/cms/Elsa.OrchardCore/ActivityProviders/OrchardContentItemsEventActivityProvider.cs index 43f77d3e..35b1c7f6 100644 --- a/src/modules/cms/Elsa.OrchardCore/ActivityProviders/OrchardContentItemsEventActivityProvider.cs +++ b/src/modules/cms/Elsa.OrchardCore/ActivityProviders/OrchardContentItemsEventActivityProvider.cs @@ -50,11 +50,12 @@ private async Task CreateActivityDescriptorAsync(string cont activityDescriptor.Description = description; activityDescriptor.Constructor = context => { - var activity = context.CreateActivity(); + var result = context.CreateActivity(); + var activity = result.Activity; activity.Type = fullTypeName; activity.ContentType = contentType; activity.EventType = eventType; - return activity; + return result; }; foreach (var inputDescriptor in activityDescriptor.Inputs) diff --git a/src/modules/http/Elsa.Http.Webhooks/ActivityProviders/WebhookEventActivityProvider.cs b/src/modules/http/Elsa.Http.Webhooks/ActivityProviders/WebhookEventActivityProvider.cs index ecef19ea..76004546 100644 --- a/src/modules/http/Elsa.Http.Webhooks/ActivityProviders/WebhookEventActivityProvider.cs +++ b/src/modules/http/Elsa.Http.Webhooks/ActivityProviders/WebhookEventActivityProvider.cs @@ -41,11 +41,12 @@ private async Task CreateActivityDescriptorAsync(WebhookSour activityDescriptor.Description = eventTypeDescription; activityDescriptor.Constructor = context => { - var activity = context.CreateActivity(); + var result = context.CreateActivity(); + var activity = result.Activity; activity.Type = fullTypeName; activity.EventType = eventType.EventType; activity.PayloadType = eventType.PayloadType; - return activity; + return result; }; var eventTypeDescriptor = activityDescriptor.Inputs.First(x => x.Name == nameof(WebhookEventReceived.EventType)); diff --git a/src/modules/servicebus/Elsa.ServiceBus.MassTransit/Services/MassTransitActivityTypeProvider.cs b/src/modules/servicebus/Elsa.ServiceBus.MassTransit/Services/MassTransitActivityTypeProvider.cs index 30f92f83..8ebeaf8d 100644 --- a/src/modules/servicebus/Elsa.ServiceBus.MassTransit/Services/MassTransitActivityTypeProvider.cs +++ b/src/modules/servicebus/Elsa.ServiceBus.MassTransit/Services/MassTransitActivityTypeProvider.cs @@ -75,10 +75,11 @@ private async Task CreateMessageReceivedDescriptor(Type mess }, Constructor = context => { - var activity = context.CreateActivity(); + var result = context.CreateActivity(); + var activity = result.Activity; activity.Type = fullTypeName; activity.MessageType = messageType; - return activity; + return result; } }; } @@ -117,10 +118,11 @@ private async Task CreatePublishMessageDescriptor(Type messa }, Constructor = context => { - var activity = context.CreateActivity(); + var result = context.CreateActivity(); + var activity = result.Activity; activity.Type = fullTypeName; activity.MessageType = messageType; - return activity; + return result; } }; } From c34a90f99110bf626233f7b30103a8e0c6b876c5 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 23 Jan 2026 19:06:48 +0100 Subject: [PATCH 4/8] Add migration for additional activity execution record fields Added a new FluentMigrator migration to include extra fields in the `ActivityExecutionRecords` table, enabling enhanced metadata, scheduling, and call stack depth tracking. --- .../Runtime/V3_7.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs diff --git a/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs b/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs new file mode 100644 index 00000000..46e58914 --- /dev/null +++ b/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs @@ -0,0 +1,33 @@ +using System.Diagnostics.CodeAnalysis; +using FluentMigrator; +using JetBrains.Annotations; +using static System.Int32; + +namespace Elsa.Persistence.Dapper.Migrations.Runtime; + +/// +[Migration(20007, "Elsa:Runtime:V3.7")] +[PublicAPI] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public class V3_7 : Migration +{ + /// + public override void Up() + { + Alter.Table("ActivityExecutionRecords").AddColumn("SerializedMetadata").AsString(MaxValue).Nullable(); + Alter.Table("ActivityExecutionRecords").AddColumn("SchedulingActivityExecutionId").AsString().Nullable(); + Alter.Table("ActivityExecutionRecords").AddColumn("SchedulingActivityId").AsString().Nullable(); + Alter.Table("ActivityExecutionRecords").AddColumn("SchedulingWorkflowInstanceId").AsString().Nullable(); + Alter.Table("ActivityExecutionRecords").AddColumn("CallStackDepth").AsInt32().Nullable(); + } + + /// + public override void Down() + { + Delete.Column("SerializedMetadata").FromTable("ActivityExecutionRecords"); + Delete.Column("SchedulingActivityExecutionId").FromTable("ActivityExecutionRecords"); + Delete.Column("SchedulingActivityId").FromTable("ActivityExecutionRecords"); + Delete.Column("SchedulingWorkflowInstanceId").FromTable("ActivityExecutionRecords"); + Delete.Column("CallStackDepth").FromTable("ActivityExecutionRecords"); + } +} From 6329784d0ba192cd8356a99fd71c2322cf6be3a4 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 4 Feb 2026 21:09:59 +0100 Subject: [PATCH 5/8] Fix infinite loop issues in activity execution chain traversal Added cycle detection using a `HashSet` to prevent infinite loops when traversing activity execution chains in multiple storage implementations. Updated unit tests to validate correct handling of circular references and chain traversal. --- .../Runtime/Stores/DapperActivityExecutionRecordStore.cs | 3 ++- .../Modules/Runtime/ActivityExecutionLogStore.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs index dc50ede5..d351e825 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs @@ -97,10 +97,11 @@ public async Task> GetExecutionChainAsync( CancellationToken cancellationToken = default) { var chain = new List(); + var visited = new HashSet(); var currentId = activityExecutionId; // Traverse the chain backwards from the specified record to the root. - while (currentId != null) + while (currentId != null && visited.Add(currentId)) { var id = currentId; var record = await store.FindAsync(q => q.Is(nameof(ActivityExecutionRecordRecord.Id), id), cancellationToken); diff --git a/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs b/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs index f39b334c..c8e17494 100644 --- a/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs +++ b/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs @@ -83,10 +83,11 @@ public async Task> GetExecutionChainAsync( CancellationToken cancellationToken = default) { var chain = new List(); + var visited = new HashSet(); var currentId = activityExecutionId; // Traverse the chain backwards from the specified record to the root. - while (currentId != null) + while (currentId != null && visited.Add(currentId)) { var id = currentId; var record = await mongoDbStore.FindAsync(queryable => queryable.Where(x => x.Id == id), cancellationToken); From 758946ad2d4da502cc96f64006be11951a15996b Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Wed, 4 Feb 2026 21:18:32 +0100 Subject: [PATCH 6/8] Refactor activity execution chain retrieval logic Centralized the `GetExecutionChainAsync` method into an extension class to streamline and unify its implementation across stores. Removed redundant implementations from individual stores and updated interfaces to utilize the new extension method. This reduces code duplication and simplifies future maintenance. --- .../DapperActivityExecutionRecordStore.cs | 43 ------------------- .../Runtime/ActivityExecutionLogStore.cs | 42 ------------------ 2 files changed, 85 deletions(-) diff --git a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs index d351e825..c2bace88 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper/Modules/Runtime/Stores/DapperActivityExecutionRecordStore.cs @@ -89,49 +89,6 @@ public async Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Ca return await store.DeleteAsync(q => ApplyFilter(q, filter), cancellationToken); } - public async Task> GetExecutionChainAsync( - string activityExecutionId, - bool includeCrossWorkflowChain = true, - int? skip = null, - int? take = null, - CancellationToken cancellationToken = default) - { - var chain = new List(); - var visited = new HashSet(); - var currentId = activityExecutionId; - - // Traverse the chain backwards from the specified record to the root. - while (currentId != null && visited.Add(currentId)) - { - var id = currentId; - var record = await store.FindAsync(q => q.Is(nameof(ActivityExecutionRecordRecord.Id), id), cancellationToken); - - if (record == null) - break; - - var mappedRecord = Map(record); - chain.Add(mappedRecord); - - // If not including cross-workflow chain and we hit a workflow boundary, stop. - if (!includeCrossWorkflowChain && mappedRecord.SchedulingWorkflowInstanceId != null) - break; - - currentId = mappedRecord.SchedulingActivityExecutionId; - } - - // Reverse to get root-to-leaf order. - chain.Reverse(); - - var totalCount = chain.Count; - - // Apply pagination if specified. - if (skip.HasValue) - chain = chain.Skip(skip.Value).ToList(); - if (take.HasValue) - chain = chain.Take(take.Value).ToList(); - - return Page.Of(chain, totalCount); - } private static void ApplyFilter(ParameterizedQuery query, ActivityExecutionRecordFilter filter) { diff --git a/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs b/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs index c8e17494..0c7d09ec 100644 --- a/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs +++ b/src/modules/persistence/Elsa.Persistence.MongoDb/Modules/Runtime/ActivityExecutionLogStore.cs @@ -75,48 +75,6 @@ public Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Cancella return mongoDbStore.DeleteWhereAsync(queryable => Filter(queryable, filter), x => x.Id, cancellationToken); } - public async Task> GetExecutionChainAsync( - string activityExecutionId, - bool includeCrossWorkflowChain = true, - int? skip = null, - int? take = null, - CancellationToken cancellationToken = default) - { - var chain = new List(); - var visited = new HashSet(); - var currentId = activityExecutionId; - - // Traverse the chain backwards from the specified record to the root. - while (currentId != null && visited.Add(currentId)) - { - var id = currentId; - var record = await mongoDbStore.FindAsync(queryable => queryable.Where(x => x.Id == id), cancellationToken); - - if (record == null) - break; - - chain.Add(record); - - // If not including cross-workflow chain and we hit a workflow boundary, stop. - if (!includeCrossWorkflowChain && record.SchedulingWorkflowInstanceId != null) - break; - - currentId = record.SchedulingActivityExecutionId; - } - - // Reverse to get root-to-leaf order. - chain.Reverse(); - - var totalCount = chain.Count; - - // Apply pagination if specified. - if (skip.HasValue) - chain = chain.Skip(skip.Value).ToList(); - if (take.HasValue) - chain = chain.Take(take.Value).ToList(); - - return Page.Of(chain, totalCount); - } private IQueryable Filter(IQueryable queryable, ActivityExecutionRecordFilter filter) { From 658406fec961964bf1d7fcd744dfe9e157b28156 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:37:13 +0000 Subject: [PATCH 7/8] Initial plan From 22ee4208b452bdc4b22888e67866bb7ed576e67a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:40:43 +0000 Subject: [PATCH 8/8] Add missing AggregateFaultCount column to migration Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com> --- .../Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs b/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs index 46e58914..1c990c92 100644 --- a/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs +++ b/src/modules/persistence/Elsa.Persistence.Dapper.Migrations/Runtime/V3_7.cs @@ -19,6 +19,7 @@ public override void Up() Alter.Table("ActivityExecutionRecords").AddColumn("SchedulingActivityId").AsString().Nullable(); Alter.Table("ActivityExecutionRecords").AddColumn("SchedulingWorkflowInstanceId").AsString().Nullable(); Alter.Table("ActivityExecutionRecords").AddColumn("CallStackDepth").AsInt32().Nullable(); + Alter.Table("ActivityExecutionRecords").AddColumn("AggregateFaultCount").AsInt32().NotNullable().WithDefaultValue(0); } /// @@ -29,5 +30,6 @@ public override void Down() Delete.Column("SchedulingActivityId").FromTable("ActivityExecutionRecords"); Delete.Column("SchedulingWorkflowInstanceId").FromTable("ActivityExecutionRecords"); Delete.Column("CallStackDepth").FromTable("ActivityExecutionRecords"); + Delete.Column("AggregateFaultCount").FromTable("ActivityExecutionRecords"); } }