diff --git a/Elsa.sln b/Elsa.sln index c1c55e7517..079a69ea42 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -19,6 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D icon.png = icon.png NuGet.Config = NuGet.Config README.md = README.md + plan-activityExecutionCallStack.prompt.md = plan-activityExecutionCallStack.prompt.md EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}" diff --git a/plan-activityExecutionCallStack.prompt.md b/plan-activityExecutionCallStack.prompt.md new file mode 100644 index 0000000000..4178261fe0 --- /dev/null +++ b/plan-activityExecutionCallStack.prompt.md @@ -0,0 +1,212 @@ +# Plan: Activity Execution Call Stack Implementation (Hybrid: explicit + ambient) + +This plan implements a comprehensive call stack mechanism to track the execution chain from root workflow through all parent activities to a specific activity execution, enabling visibility into the complete invocation hierarchy when viewing activity execution records. + +Core design +- Explicit predecessor: Activities that know the causal predecessor (e.g., a completed child that schedules the next) set `SchedulingActivityExecutionId` directly via scheduling options. +- Ambient fallback: During completion callbacks, bookmark resumes, and child-workflow starts, the workflow sets an ambient "current scheduling source" on the `WorkflowExecutionContext`. If a schedule call omits `SchedulingActivityExecutionId`, the scheduler fills it from the ambient. This minimizes code churn while preserving correctness. +- Structural vs temporal: Keep `Owner`/`ParentActivityExecutionContext` for structural containment; use `SchedulingActivityExecutionId`/`SchedulingWorkflowInstanceId` for the temporal execution chain. + +## Steps + +### 1. Add call stack fields to scheduling models throughout the chain + +Add `SchedulingActivityExecutionId` (nullable `string`) to: +- [`ScheduleWorkOptions`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Options/ScheduleWorkOptions.cs) +- [`ScheduledActivityOptions`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Models/ScheduledActivityOptions.cs) +- [`ActivityWorkItem`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Models/ActivityWorkItem.cs) +- [`ActivityInvocationOptions`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Options/ActivityInvocationOptions.cs) + +Include clear XML documentation explaining this tracks the temporal/execution predecessor (distinct from structural `Owner`/`ParentActivityExecutionContext`). + +Update all constructors and property mappings in: +- [`WorkflowExecutionContextSchedulerStrategy.Schedule`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Services/WorkflowExecutionContextSchedulerStrategy.cs) +- [`DefaultActivitySchedulerMiddleware.ExecuteWorkItemAsync`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs) + +Thread this value through the scheduling chain. + +### 2. Store call stack fields in runtime and persisted contexts + +Add the following fields to [`ActivityExecutionContext`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs): +- `SchedulingActivityExecutionId` (nullable `string`) +- `SchedulingActivityId` (nullable `string` - denormalized for convenience) +- `SchedulingWorkflowInstanceId` (nullable `string` - for cross-workflow tracking) + +Include XML comments distinguishing these from `ParentActivityExecutionContext`: +- **`ParentActivityExecutionContext`**: The structural container activity (e.g., Flowchart contains all its children). Represents the hierarchical parent in the workflow structure. +- **`SchedulingActivityExecutionId`**: The temporal/execution predecessor that directly triggered execution of this activity. Tracks the execution sequence, not the structural hierarchy. +- **`SchedulingWorkflowInstanceId`**: The workflow instance ID of the activity that invoked this activity's workflow. Set when crossing workflow boundaries (e.g., via `ExecuteWorkflow` or `DispatchWorkflow`). + +Update [`WorkflowExecutionContext.CreateActivityExecutionContextAsync`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs) to accept and store these fields from `ActivityInvocationOptions`. + +Add corresponding fields to [`ActivityExecutionRecord`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs): +- `SchedulingActivityExecutionId` (nullable `string`) +- `SchedulingActivityId` (nullable `string`) +- `SchedulingWorkflowInstanceId` (nullable `string`) +- `CallStackDepth` (nullable `int`) + +Update [`DefaultActivityExecutionMapper.MapAsync`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs) to populate these fields from the execution context. Calculate `CallStackDepth` by traversing the `SchedulingActivityExecutionId` chain until reaching null. + +### 3. Add ambient scheduling source to the workflow context + +Add an ambient scheduling source to [`WorkflowExecutionContext`]: +- Fields (transient): `CurrentSchedulingActivityExecutionId`, `CurrentSchedulingWorkflowInstanceId`. +- API: `IDisposable BeginSchedulingScope(string? activityExecutionId, string? workflowInstanceId)` that pushes values and restores previous values on dispose. + +Set ambient scope in these places: +- Around owner completion callbacks (where next activities are scheduled). +- Around bookmark-resume handlers (background completions resuming the workflow). +- At child-workflow start (root activity creation). + +Modify [`WorkflowExecutionContextSchedulerStrategy.Schedule`] to set `SchedulingActivityExecutionId` and `SchedulingWorkflowInstanceId` from `ScheduleWorkOptions` if provided; otherwise fall back to the ambient `WorkflowExecutionContext` values. + +### 4. Update composite activities to capture scheduling activity context + +In [`Flowchart.Counters.cs`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs): +- Update `ScheduleOutboundActivityAsync` to populate `ScheduleWorkOptions.SchedulingActivityExecutionId = completedActivityContext.Id` when a completed activity schedules its outbound activities. +- Update `MaybeScheduleBackwardConnectionActivityAsync` to include `SchedulingActivityExecutionId` in the `ScheduleWorkOptions`. +- Update `MaybeScheduleWaitAllActivityAsync`, `MaybeScheduleWaitAllActiveActivityAsync`, `MaybeScheduleWaitAnyActivityAsync` to pass `SchedulingActivityExecutionId` when scheduling. + +In [`Flowchart.Tokens.cs`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs): +- Update `OnChildCompletedTokenBasedLogicAsync` to pass `SchedulingActivityExecutionId` in `ScheduleWorkOptions` when scheduling subsequent activities. + +Apply the same pattern to other composite activities: +- `Sequence` +- `ForEach` +- `Parallel` +- `While` +- `Do` +- Any other activities that schedule child activities based on completion + +When scheduling a child activity that was directly triggered by another activity's completion, set `SchedulingActivityExecutionId` to the completing activity's execution context ID. When omitted, the ambient scope ensures a sensible fallback. + +### 5. Handle cross-workflow call stack linkage (span by default) + +For [`ExecuteWorkflow`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs) and [`DispatchWorkflow`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs): + +- Capture the calling activity's `ExecutionId` and current `WorkflowInstanceId`. +- Pass them through `RunWorkflowOptions` and `DispatchWorkflowRequest` to the child workflow. +- When the child workflow starts, set the first activity's: + - `SchedulingActivityExecutionId` to the parent's invocation activity's execution ID. + - `SchedulingWorkflowInstanceId` to the parent workflow instance ID. +- Also set the ambient scope (`BeginSchedulingScope`) for the duration of child start so subsequent schedules inherit these values by default. + +Cross-workflow chains should be considered part of the call stack by default (span by default). + +### 6. Add call stack depth and create database migration + +Add `CallStackDepth` (nullable `int`) field to [`ActivityExecutionRecord`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs). + +In [`DefaultActivityExecutionMapper.MapAsync`](file:///Users/sipke/Projects/Elsa/elsa-core/main/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs): +- Calculate `CallStackDepth` by traversing the source `ActivityExecutionContext.SchedulingActivityExecutionId` chain until reaching null. +- Use root depth = 0 (documented convention). +- Store this value in the `ActivityExecutionRecord`. + +Create EF Core migration adding indexed columns to `ActivityExecutionRecord` table: +- `SchedulingActivityExecutionId` (indexed, nullable) +- `SchedulingActivityId` (indexed, nullable) +- `SchedulingWorkflowInstanceId` (indexed, nullable) +- `CallStackDepth` (indexed, nullable) + +The `CallStackDepth` index enables efficient filtering by execution depth without reconstructing the full chain (e.g., "show me all activities at depth > 5"). + +### 7. Implement call stack query and reconstruction APIs + +Implement `IActivityExecutionStore.GetExecutionChainAsync(string activityExecutionId, bool includeCrossWorkflowChain = true, int? skip = null, int? take = null)` that: +- Recursively queries `SchedulingActivityExecutionId` until reaching root (null). +- Follows `SchedulingWorkflowInstanceId` across workflow boundaries by default (span by default). Optionally allow disabling cross-workflow span via parameter. +- Supports pagination via `skip` and `take` parameters to handle deep call stacks efficiently. +- Returns a paginated result containing: + - `Items`: List of execution records (ordered from root to current activity, or subset if paginated) + - `TotalCount`: Total number of items in the full chain + - `Skip`: The skip value used + - `Take`: The take value used +- When pagination is not specified (`skip` and `take` are null), returns the full chain. + +Add extension methods: +- **`ActivityExecutionContext.GetExecutionChain(int? skip = null, int? take = null)`**: Reconstruct the runtime call stack by traversing `SchedulingActivityExecutionId`, returning a paginated result from root to current activity. +- **`ActivityExecutionRecord.GetExecutionChainAsync(IActivityExecutionStore, bool includeCrossWorkflowChain = true, int? skip = null, int? take = null)`**: Reconstruct persisted call stacks by querying the store with pagination support. + +Both methods should return results ordered from root to current activity, with pagination applied after ordering. + +Add REST API endpoint: +- **`GET /api/workflow-instances/{workflowInstanceId}/activity-executions/{activityExecutionId}/call-chain`** + - Query parameters: + - `includeCrossWorkflowChain` (bool, default: true): Include parent workflow activities across workflow boundaries + - `skip` (int?, optional): Number of items to skip (for pagination) + - `take` (int?, optional): Number of items to return (for pagination, recommended max: 100) + - Response: + - `items`: Array of activity execution records + - `totalCount`: Total number of items in the full chain + - `skip`: The skip value used + - `take`: The take value used (or null if full chain returned) + - This enables UI to implement paginated/lazy loading for deep call stacks. + +## Further Considerations + +### 1. Structural vs temporal hierarchy documentation + +`ParentActivityExecutionContext` represents the structural container (e.g., Flowchart contains all its children), while `SchedulingActivityExecutionId` tracks the temporal execution predecessor (e.g., Activity B completed and directly triggered Activity C). + +These are orthogonal relationships: +- A Flowchart can own many children (structural), but only a predecessor directly triggers the next (temporal). +- When Activity B completes, it schedules the next child, establishing a temporal link via `SchedulingActivityExecutionId`. + +All XML comments for these fields should explicitly clarify this distinction to prevent developer confusion and misuse. + +### 2. Ambient scope guardrails + +- The ambient scope must be short-lived and always disposed via `using`/`finally` to avoid leakage between unrelated scheduling operations. +- The scheduler should prefer explicit `ScheduleWorkOptions.SchedulingActivityExecutionId`/`SchedulingWorkflowInstanceId` and only fall back to ambient when not provided. +- Document that the ambient exists to reduce boilerplate and should not be relied upon when explicit causal context is readily available. + +### 3. Cross-workflow boundary reconstruction (default span) + +`SchedulingWorkflowInstanceId` enables reconstructing call stacks that span multiple workflow instances: +- Parent Workflow (Instance A) → ExecuteWorkflow activity (in Instance A) → Child Workflow (Instance B) → failing activity (in Instance B). + +Since span is the default, cross-instance traversal should occur unless explicitly disabled. + +### 4. Call stack depth optimization trade-offs + +Storing `CallStackDepth` trades a small amount of storage for simpler, faster queries and analytics: + +**Benefits:** +- Efficient filtering by depth ranges (e.g., "depth > 5"). +- Early termination in chain reconstruction. +- Index-based analytics queries. +- Lower storage than persisting full chains. + +**Drawbacks:** +- `CallStackDepth` becomes stale if parent records are deleted or altered post-hoc. Prefer immutable execution records. +- Document that `CallStackDepth` is an optimization hint for querying, not an authoritative source if retention policies prune ancestors. + +### 5. Testing matrix + +- Sequential flow: A → B → C (explicit predecessor set, ambient unused). +- Parallel fan-out: A schedules B and C (both record A as predecessor; ambient vs explicit). +- Nested composites: Multiple owners scheduling into the same queue. +- Background resume: Bookmark-based resumes interleaving with other work; ambient set during resume. +- Cross-workflow: Execute/Dispatch child workflow; default spanning chain. +- Deduplication scenarios: `PreventDuplicateScheduling` and re-scheduling. +- Persistence/round-trips: Background/persisted scheduled activities using `ScheduledActivityOptions`. +- Deep call stacks: Test pagination with chains deeper than 100 activities. +- Cross-workflow pagination: Ensure pagination works correctly when spanning workflow boundaries. + +### 6. Performance and pagination considerations + +**Deep call stack handling:** +- For very deep call stacks (e.g., recursive workflows or long-running sequential processes), retrieving the entire chain in a single query can be expensive. +- Pagination (`skip`/`take`) enables efficient loading in the UI with incremental/lazy loading patterns. +- Recommend default `take` of 50-100 items per page for REST API calls. +- The `CallStackDepth` field enables quick assessment of chain depth before deciding whether to paginate. + +**Query optimization strategies:** +- Use indexed lookups on `SchedulingActivityExecutionId` to traverse the chain efficiently. +- Consider caching strategies for frequently accessed chains (e.g., recently failed activities). +- For cross-workflow queries, implement efficient join strategies or batched lookups to minimize round-trips. +- Document that pagination is "forward-only" (skip/take from root toward current) to align with typical debugging workflows (start at root, drill down). + +**REST API rate limiting:** +- Consider rate limiting on the call chain endpoint if it becomes a performance bottleneck. +- Monitor query performance and adjust default pagination sizes based on observed data. diff --git a/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Contracts/IActivityExecutionsApi.cs b/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Contracts/IActivityExecutionsApi.cs index 2238c81621..65266fb05d 100644 --- a/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Contracts/IActivityExecutionsApi.cs +++ b/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Contracts/IActivityExecutionsApi.cs @@ -45,4 +45,16 @@ public interface IActivityExecutionsApi /// The activity execution. [Get("/activity-executions/{id}")] Task GetAsync(string id, CancellationToken cancellationToken = default); -} \ No newline at end of file + + /// + /// Gets the call stack (execution chain) for a given activity execution. + /// + /// The ID of the activity execution. + /// Whether to include parent workflow activities across workflow boundaries. + /// The number of items to skip (for pagination). + /// The maximum number of items to return (for pagination). + /// An optional cancellation token. + /// The response containing the call stack. + [Get("/activity-executions/{id}/call-stack")] + Task GetCallStackAsync(string id, bool? includeCrossWorkflowChain = null, int? skip = null, int? take = null, CancellationToken cancellationToken = default); +} diff --git a/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionCallStack.cs b/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionCallStack.cs new file mode 100644 index 0000000000..a261655245 --- /dev/null +++ b/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionCallStack.cs @@ -0,0 +1,22 @@ +namespace Elsa.Api.Client.Resources.ActivityExecutions.Models; + +/// +/// Represents a call stack (execution chain) for a given activity execution. +/// +public class ActivityExecutionCallStack +{ + /// + /// The ID of the activity execution that was requested. + /// + public string ActivityExecutionId { get; set; } = null!; + + /// + /// The activity execution records in the call stack (ordered from root to current activity). + /// + public ICollection Items { get; set; } = new List(); + + /// + /// The total number of items in the full call stack chain. + /// + public long TotalCount { get; set; } +} diff --git a/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionRecord.cs b/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionRecord.cs index c3ac7c87c8..81346b9994 100644 --- a/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionRecord.cs +++ b/src/clients/Elsa.Api.Client/Resources/ActivityExecutions/Models/ActivityExecutionRecord.cs @@ -85,9 +85,37 @@ public class ActivityExecutionRecord : Entity /// Gets or sets the status of the activity. /// public ActivityStatus Status { 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 + + /// + /// The ID of the activity execution context that scheduled this activity execution. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The ID of the activity that scheduled this activity execution (denormalized for convenience). + /// + public string? SchedulingActivityId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this activity execution. + /// This is set when crossing workflow boundaries (e.g., via ExecuteWorkflow or DispatchWorkflow). + /// + public string? SchedulingWorkflowInstanceId { get; set; } + + /// + /// The depth of this activity in the call stack (0 for root activities). + /// Calculated by traversing the SchedulingActivityExecutionId chain until reaching null. + /// + public int? CallStackDepth { get; set; } +} diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20260122123013_V3_7.Designer.cs b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20260122123013_V3_7.Designer.cs new file mode 100644 index 0000000000..7c4a0dc74b --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20260122123013_V3_7.Designer.cs @@ -0,0 +1,522 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20260122123013_V3_7")] + partial class V3_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityName") + .HasColumnType("varchar(255)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("AggregateFaultCount") + .HasColumnType("int"); + + b.Property("CallStackDepth") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("HasBookmarks") + .HasColumnType("tinyint(1)"); + + b.Property("SchedulingActivityExecutionId") + .HasColumnType("longtext"); + + b.Property("SchedulingActivityId") + .HasColumnType("longtext"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("longtext"); + + b.Property("SerializedActivityState") + .HasColumnType("longtext"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("longtext"); + + b.Property("SerializedException") + .HasColumnType("longtext"); + + b.Property("SerializedMetadata") + .HasColumnType("longtext"); + + b.Property("SerializedOutputs") + .HasColumnType("longtext"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("SerializedProperties") + .HasColumnType("longtext"); + + b.Property("StartedAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeName") + .HasColumnType("varchar(255)"); + + b.Property("BookmarkId") + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerializedOptions") + .HasColumnType("longtext"); + + b.Property("StimulusHash") + .HasColumnType("varchar(255)"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("SerializedMetadata") + .HasColumnType("longtext"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("Hash") + .HasColumnType("varchar(255)"); + + b.Property("Name") + .HasColumnType("varchar(255)"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityName") + .HasColumnType("varchar(255)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("EventName") + .HasColumnType("varchar(255)"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("Sequence") + .HasColumnType("bigint"); + + b.Property("SerializedActivityState") + .HasColumnType("longtext"); + + b.Property("SerializedPayload") + .HasColumnType("longtext"); + + b.Property("Source") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("varchar(255)"); + + b.Property("Timestamp") + .HasColumnType("datetime(6)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("WorkflowVersion") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("varchar(255)"); + + b.Property("ActivityInstanceId") + .HasColumnType("varchar(255)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("CorrelationId") + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("varchar(255)"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("longtext"); + + b.Property("SerializedInput") + .HasColumnType("longtext"); + + b.Property("TenantId") + .HasColumnType("longtext"); + + b.Property("WorkflowInstanceId") + .HasColumnType("varchar(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20260122123013_V3_7.cs b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20260122123013_V3_7.cs new file mode 100644 index 0000000000..84c98e2a4d --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/20260122123013_V3_7.cs @@ -0,0 +1,77 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.MySql.Migrations.Runtime +{ + /// + public partial class V3_7 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_7(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "longtext", + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index f7add71af7..028fa234ad 100644 --- a/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.MySql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -68,12 +68,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregateFaultCount") .HasColumnType("int"); + b.Property("CallStackDepth") + .HasColumnType("int"); + b.Property("CompletedAt") .HasColumnType("datetime(6)"); b.Property("HasBookmarks") .HasColumnType("tinyint(1)"); + b.Property("SchedulingActivityExecutionId") + .HasColumnType("longtext"); + + b.Property("SchedulingActivityId") + .HasColumnType("longtext"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("longtext"); + b.Property("SerializedActivityState") .HasColumnType("longtext"); diff --git a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20260122123056_V3_7.Designer.cs b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20260122123056_V3_7.Designer.cs new file mode 100644 index 0000000000..3e903ac43e --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20260122123056_V3_7.Designer.cs @@ -0,0 +1,523 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Oracle.EntityFrameworkCore.Metadata; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20260122123056_V3_7")] + partial class V3_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + OracleModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("NUMBER(10)"); + + b.Property("AggregateFaultCount") + .HasColumnType("NUMBER(10)"); + + b.Property("CallStackDepth") + .HasColumnType("NUMBER(10)"); + + b.Property("CompletedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("HasBookmarks") + .HasColumnType("BOOLEAN"); + + b.Property("SchedulingActivityExecutionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SchedulingActivityId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedActivityState") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedException") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedMetadata") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedOutputs") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedProperties") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("StartedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Status") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("BookmarkId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("SerializedOptions") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("StimulusHash") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Name") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedMetadata") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Hash") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Name") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId") + .HasFilter("\"Hash\" IS NOT NULL"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("NUMBER(10)"); + + b.Property("EventName") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Message") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Sequence") + .HasColumnType("NUMBER(19)"); + + b.Property("SerializedActivityState") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("Source") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("Timestamp") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("WorkflowVersion") + .HasColumnType("NUMBER(10)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CorrelationId") + .HasColumnType("NVARCHAR2(450)"); + + b.Property("CreatedAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("ExpiresAt") + .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("NVARCHAR2(450)"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SerializedInput") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("TenantId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("NVARCHAR2(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20260122123056_V3_7.cs b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20260122123056_V3_7.cs new file mode 100644 index 0000000000..e1d892977a --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/20260122123056_V3_7.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Oracle.Migrations.Runtime +{ + /// + public partial class V3_7 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_7(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "NUMBER(10)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "NVARCHAR2(2000)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "NVARCHAR2(2000)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "NVARCHAR2(2000)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index cdee3d63e3..f348912f6a 100644 --- a/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.Oracle/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -68,12 +68,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregateFaultCount") .HasColumnType("NUMBER(10)"); + b.Property("CallStackDepth") + .HasColumnType("NUMBER(10)"); + b.Property("CompletedAt") .HasColumnType("TIMESTAMP(7) WITH TIME ZONE"); b.Property("HasBookmarks") .HasColumnType("BOOLEAN"); + b.Property("SchedulingActivityExecutionId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SchedulingActivityId") + .HasColumnType("NVARCHAR2(2000)"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("NVARCHAR2(2000)"); + b.Property("SerializedActivityState") .HasColumnType("NCLOB"); diff --git a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20260122123049_V3_7.Designer.cs b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20260122123049_V3_7.Designer.cs new file mode 100644 index 0000000000..3120b6882e --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20260122123049_V3_7.Designer.cs @@ -0,0 +1,522 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20260122123049_V3_7")] + partial class V3_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityName") + .HasColumnType("text"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityTypeVersion") + .HasColumnType("integer"); + + b.Property("AggregateFaultCount") + .HasColumnType("integer"); + + b.Property("CallStackDepth") + .HasColumnType("integer"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("HasBookmarks") + .HasColumnType("boolean"); + + b.Property("SchedulingActivityExecutionId") + .HasColumnType("text"); + + b.Property("SchedulingActivityId") + .HasColumnType("text"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("text"); + + b.Property("SerializedActivityState") + .HasColumnType("text"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("text"); + + b.Property("SerializedException") + .HasColumnType("text"); + + b.Property("SerializedMetadata") + .HasColumnType("text"); + + b.Property("SerializedOutputs") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("SerializedProperties") + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .HasColumnType("text"); + + b.Property("ActivityTypeName") + .HasColumnType("text"); + + b.Property("BookmarkId") + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SerializedOptions") + .HasColumnType("text"); + + b.Property("StimulusHash") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .HasColumnType("text"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("SerializedMetadata") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Hash") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityName") + .HasColumnType("text"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("text"); + + b.Property("ActivityTypeVersion") + .HasColumnType("integer"); + + b.Property("EventName") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("text"); + + b.Property("Sequence") + .HasColumnType("bigint"); + + b.Property("SerializedActivityState") + .HasColumnType("text"); + + b.Property("SerializedPayload") + .HasColumnType("text"); + + b.Property("Source") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("WorkflowVersion") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ActivityInstanceId") + .HasColumnType("text"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CorrelationId") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("text"); + + b.Property("SerializedInput") + .HasColumnType("text"); + + b.Property("TenantId") + .HasColumnType("text"); + + b.Property("WorkflowInstanceId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20260122123049_V3_7.cs b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20260122123049_V3_7.cs new file mode 100644 index 0000000000..b1e2e7dfd1 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/20260122123049_V3_7.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.PostgreSql.Migrations.Runtime +{ + /// + public partial class V3_7 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_7(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "integer", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 6b6665f499..9707b9c3a7 100644 --- a/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.PostgreSql/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -68,12 +68,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregateFaultCount") .HasColumnType("integer"); + b.Property("CallStackDepth") + .HasColumnType("integer"); + b.Property("CompletedAt") .HasColumnType("timestamp with time zone"); b.Property("HasBookmarks") .HasColumnType("boolean"); + b.Property("SchedulingActivityExecutionId") + .HasColumnType("text"); + + b.Property("SchedulingActivityId") + .HasColumnType("text"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("text"); + b.Property("SerializedActivityState") .HasColumnType("text"); diff --git a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20260122123023_V3_7.Designer.cs b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20260122123023_V3_7.Designer.cs new file mode 100644 index 0000000000..8f0196bdb3 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20260122123023_V3_7.Designer.cs @@ -0,0 +1,523 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20260122123023_V3_7")] + partial class V3_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityName") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("AggregateFaultCount") + .HasColumnType("int"); + + b.Property("CallStackDepth") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("HasBookmarks") + .HasColumnType("bit"); + + b.Property("SchedulingActivityExecutionId") + .HasColumnType("nvarchar(max)"); + + b.Property("SchedulingActivityId") + .HasColumnType("nvarchar(max)"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedActivityState") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedException") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedMetadata") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedOutputs") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedProperties") + .HasColumnType("nvarchar(max)"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeName") + .HasColumnType("nvarchar(450)"); + + b.Property("BookmarkId") + .HasColumnType("nvarchar(450)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("SerializedOptions") + .HasColumnType("nvarchar(max)"); + + b.Property("StimulusHash") + .HasColumnType("nvarchar(450)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedMetadata") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Hash") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId") + .HasFilter("[Hash] IS NOT NULL"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityName") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeVersion") + .HasColumnType("int"); + + b.Property("EventName") + .HasColumnType("nvarchar(450)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("Sequence") + .HasColumnType("bigint"); + + b.Property("SerializedActivityState") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("Source") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(450)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("WorkflowVersion") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityInstanceId") + .HasColumnType("nvarchar(450)"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("CorrelationId") + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("nvarchar(max)"); + + b.Property("SerializedInput") + .HasColumnType("nvarchar(max)"); + + b.Property("TenantId") + .HasColumnType("nvarchar(max)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20260122123023_V3_7.cs b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20260122123023_V3_7.cs new file mode 100644 index 0000000000..27dea1281d --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/20260122123023_V3_7.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.SqlServer.Migrations.Runtime +{ + /// + public partial class V3_7 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_7(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "int", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 47d2ae7cd3..ae32aec9ca 100644 --- a/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.SqlServer/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -68,12 +68,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregateFaultCount") .HasColumnType("int"); + b.Property("CallStackDepth") + .HasColumnType("int"); + b.Property("CompletedAt") .HasColumnType("datetimeoffset"); b.Property("HasBookmarks") .HasColumnType("bit"); + b.Property("SchedulingActivityExecutionId") + .HasColumnType("nvarchar(max)"); + + b.Property("SchedulingActivityId") + .HasColumnType("nvarchar(max)"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("nvarchar(max)"); + b.Property("SerializedActivityState") .HasColumnType("nvarchar(max)"); diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20260122123040_V3_7.Designer.cs b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20260122123040_V3_7.Designer.cs new file mode 100644 index 0000000000..402bffcff8 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20260122123040_V3_7.Designer.cs @@ -0,0 +1,518 @@ +// +using System; +using Elsa.Persistence.EFCore.Modules.Runtime; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Runtime +{ + [DbContext(typeof(RuntimeElsaDbContext))] + [Migration("20260122123040_V3_7")] + partial class V3_7 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("Elsa") + .HasAnnotation("ProductVersion", "9.0.11"); + + modelBuilder.Entity("Elsa.KeyValues.Entities.SerializedKeyValuePair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("SerializedValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "TenantId" }, "IX_SerializedKeyValuePair_TenantId"); + + b.ToTable("KeyValuePairs", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.ActivityExecutionRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityName") + .HasColumnType("TEXT"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityTypeVersion") + .HasColumnType("INTEGER"); + + b.Property("AggregateFaultCount") + .HasColumnType("INTEGER"); + + b.Property("CallStackDepth") + .HasColumnType("INTEGER"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("HasBookmarks") + .HasColumnType("INTEGER"); + + b.Property("SchedulingActivityExecutionId") + .HasColumnType("TEXT"); + + b.Property("SchedulingActivityId") + .HasColumnType("TEXT"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("TEXT"); + + b.Property("SerializedActivityState") + .HasColumnType("TEXT"); + + b.Property("SerializedActivityStateCompressionAlgorithm") + .HasColumnType("TEXT"); + + b.Property("SerializedException") + .HasColumnType("TEXT"); + + b.Property("SerializedMetadata") + .HasColumnType("TEXT"); + + b.Property("SerializedOutputs") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("SerializedProperties") + .HasColumnType("TEXT"); + + b.Property("StartedAt") + .HasColumnType("TEXT"); + + b.Property("Status") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityTypeVersion"); + + b.HasIndex("CompletedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_CompletedAt"); + + b.HasIndex("HasBookmarks") + .HasDatabaseName("IX_ActivityExecutionRecord_HasBookmarks"); + + b.HasIndex("StartedAt") + .HasDatabaseName("IX_ActivityExecutionRecord_StartedAt"); + + b.HasIndex("Status") + .HasDatabaseName("IX_ActivityExecutionRecord_Status"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_ActivityExecutionRecord_TenantId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_ActivityExecutionRecord_WorkflowInstanceId"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_ActivityExecutionRecord_ActivityType_ActivityTypeVersion"); + + b.ToTable("ActivityExecutionRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.BookmarkQueueItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("ActivityTypeName") + .HasColumnType("TEXT"); + + b.Property("BookmarkId") + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("SerializedOptions") + .HasColumnType("TEXT"); + + b.Property("StimulusHash") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_BookmarkQueueItem_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_BookmarkQueueItem_ActivityTypeName"); + + b.HasIndex(new[] { "BookmarkId" }, "IX_BookmarkQueueItem_BookmarkId"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_BookmarkQueueItem_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_BookmarkQueueItem_CreatedAt"); + + b.HasIndex(new[] { "StimulusHash" }, "IX_BookmarkQueueItem_StimulusHash"); + + b.HasIndex(new[] { "TenantId" }, "IX_BookmarkQueueItem_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_BookmarkQueueItem_WorkflowInstanceId"); + + b.ToTable("BookmarkQueueItems", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredBookmark", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SerializedMetadata") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_StoredBookmark_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_StoredBookmark_ActivityTypeName"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash" }, "IX_StoredBookmark_ActivityTypeName_Hash"); + + b.HasIndex(new[] { "ActivityTypeName", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_ActivityTypeName_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_StoredBookmark_CreatedAt"); + + b.HasIndex(new[] { "Hash" }, "IX_StoredBookmark_Hash"); + + b.HasIndex(new[] { "Name" }, "IX_StoredBookmark_Name"); + + b.HasIndex(new[] { "Name", "Hash" }, "IX_StoredBookmark_Name_Hash"); + + b.HasIndex(new[] { "Name", "Hash", "WorkflowInstanceId" }, "IX_StoredBookmark_Name_Hash_WorkflowInstanceId"); + + b.HasIndex(new[] { "TenantId" }, "IX_StoredBookmark_TenantId"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_StoredBookmark_WorkflowInstanceId"); + + b.ToTable("Bookmarks", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.StoredTrigger", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Hash") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Hash") + .HasDatabaseName("IX_StoredTrigger_Hash"); + + b.HasIndex("Name") + .HasDatabaseName("IX_StoredTrigger_Name"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_StoredTrigger_TenantId"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_StoredTrigger_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowDefinitionId", "Hash", "ActivityId") + .IsUnique() + .HasDatabaseName("IX_StoredTrigger_Unique_WorkflowDefinitionId_Hash_ActivityId"); + + b.ToTable("Triggers", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowExecutionLogRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityName") + .HasColumnType("TEXT"); + + b.Property("ActivityNodeId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityType") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ActivityTypeVersion") + .HasColumnType("INTEGER"); + + b.Property("EventName") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ParentActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("Sequence") + .HasColumnType("INTEGER"); + + b.Property("SerializedActivityState") + .HasColumnType("TEXT"); + + b.Property("SerializedPayload") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowDefinitionVersionId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WorkflowVersion") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ActivityId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityId"); + + b.HasIndex("ActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityInstanceId"); + + b.HasIndex("ActivityName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityName"); + + b.HasIndex("ActivityNodeId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityNodeId"); + + b.HasIndex("ActivityType") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType"); + + b.HasIndex("ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityTypeVersion"); + + b.HasIndex("EventName") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_EventName"); + + b.HasIndex("ParentActivityInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ParentActivityInstanceId"); + + b.HasIndex("Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Sequence"); + + b.HasIndex("TenantId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_TenantId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp"); + + b.HasIndex("WorkflowDefinitionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionId"); + + b.HasIndex("WorkflowDefinitionVersionId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowDefinitionVersionId"); + + b.HasIndex("WorkflowInstanceId") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowInstanceId"); + + b.HasIndex("WorkflowVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_WorkflowVersion"); + + b.HasIndex("ActivityType", "ActivityTypeVersion") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_ActivityType_ActivityTypeVersion"); + + b.HasIndex("Timestamp", "Sequence") + .HasDatabaseName("IX_WorkflowExecutionLogRecord_Timestamp_Sequence"); + + b.ToTable("WorkflowExecutionLogRecords", "Elsa"); + }); + + modelBuilder.Entity("Elsa.Workflows.Runtime.Entities.WorkflowInboxMessage", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActivityInstanceId") + .HasColumnType("TEXT"); + + b.Property("ActivityTypeName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SerializedBookmarkPayload") + .HasColumnType("TEXT"); + + b.Property("SerializedInput") + .HasColumnType("TEXT"); + + b.Property("TenantId") + .HasColumnType("TEXT"); + + b.Property("WorkflowInstanceId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ActivityInstanceId" }, "IX_WorkflowInboxMessage_ActivityInstanceId"); + + b.HasIndex(new[] { "ActivityTypeName" }, "IX_WorkflowInboxMessage_ActivityTypeName"); + + b.HasIndex(new[] { "CorrelationId" }, "IX_WorkflowInboxMessage_CorrelationId"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_WorkflowInboxMessage_CreatedAt"); + + b.HasIndex(new[] { "ExpiresAt" }, "IX_WorkflowInboxMessage_ExpiresAt"); + + b.HasIndex(new[] { "Hash" }, "IX_WorkflowInboxMessage_Hash"); + + b.HasIndex(new[] { "WorkflowInstanceId" }, "IX_WorkflowInboxMessage_WorkflowInstanceId"); + + b.ToTable("WorkflowInboxMessages", "Elsa"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20260122123040_V3_7.cs b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20260122123040_V3_7.cs new file mode 100644 index 0000000000..324ddec129 --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/20260122123040_V3_7.cs @@ -0,0 +1,74 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Elsa.Persistence.EFCore.Sqlite.Migrations.Runtime +{ + /// + public partial class V3_7 : Migration + { + private readonly Elsa.Persistence.EFCore.IElsaDbContextSchema _schema; + + /// + public V3_7(Elsa.Persistence.EFCore.IElsaDbContextSchema schema) + { + _schema = schema; + } + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "CallStackDepth", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityExecutionId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingActivityId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + + migrationBuilder.DropColumn( + name: "SchedulingWorkflowInstanceId", + schema: _schema.Schema, + table: "ActivityExecutionRecords"); + } + } +} diff --git a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs index 74fbd62105..172f4cf691 100644 --- a/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs +++ b/src/modules/Elsa.Persistence.EFCore.Sqlite/Migrations/Runtime/RuntimeElsaDbContextModelSnapshot.cs @@ -64,12 +64,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AggregateFaultCount") .HasColumnType("INTEGER"); + b.Property("CallStackDepth") + .HasColumnType("INTEGER"); + b.Property("CompletedAt") .HasColumnType("TEXT"); b.Property("HasBookmarks") .HasColumnType("INTEGER"); + b.Property("SchedulingActivityExecutionId") + .HasColumnType("TEXT"); + + b.Property("SchedulingActivityId") + .HasColumnType("TEXT"); + + b.Property("SchedulingWorkflowInstanceId") + .HasColumnType("TEXT"); + b.Property("SerializedActivityState") .HasColumnType("TEXT"); diff --git a/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/ActivityExecutionLogStore.cs b/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/ActivityExecutionLogStore.cs index c345550c06..b728ac67ac 100644 --- a/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/ActivityExecutionLogStore.cs +++ b/src/modules/Elsa.Persistence.EFCore/Modules/Runtime/ActivityExecutionLogStore.cs @@ -2,6 +2,7 @@ using System.Linq.Expressions; using System.Text.Json; using Elsa.Common; +using Elsa.Common.Models; using Elsa.Common.Codecs; using Elsa.Common.Entities; using Elsa.Extensions; @@ -82,6 +83,53 @@ public async Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Ca return await store.DeleteWhereAsync(queryable => Filter(queryable, filter), cancellationToken); } + /// + [RequiresUnreferencedCode("Calls Elsa.Persistence.EFCore.Modules.Runtime.EFCoreActivityExecutionStore.OnLoadAsync")] + 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.QueryAsync( + query => query.Where(x => x.Id == id), + OnLoadAsync, + cancellationToken).FirstOrDefault(); + + 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 ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, ActivityExecutionRecord entity, CancellationToken cancellationToken) { var snapshot = entity.SerializedSnapshot; @@ -206,4 +254,4 @@ private class ShadowActivityExecutionRecordSummary : Entity public int AggregateFaultCount { get; set; } public DateTimeOffset? CompletedAt { get; set; } } -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Persistence.EFCore/efcore-3.7.sh b/src/modules/Elsa.Persistence.EFCore/efcore-3.7.sh new file mode 100755 index 0000000000..bab1cbd67b --- /dev/null +++ b/src/modules/Elsa.Persistence.EFCore/efcore-3.7.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Define the modules to update +mods=("Runtime") + +# Define the list of providers +providers=("MySql" "SqlServer" "Sqlite" "PostgreSql" "Oracle") + +# Loop through each module +for module in "${mods[@]}"; do + # Loop through each provider + for provider in "${providers[@]}"; do + providerPath="../Elsa.Persistence.EFCore.$provider" + startupProject="$providerPath/Elsa.Persistence.EFCore.$provider.csproj" + migrationsPath="Migrations/$module" + + echo "Updating migrations for $provider..." + echo "Provider path: ${providerPath:?}" + echo "Startup project: $startupProject" + echo "Migrations path: $migrationsPath" + ef-migration-runtime-schema --interface Elsa.Persistence.EFCore.IElsaDbContextSchema --efOptions "migrations add V3_7 -c ""$module""ElsaDbContext -p ""$providerPath"" -o ""$migrationsPath"" --startup-project ""$startupProject""" + done +done diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Endpoint.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Endpoint.cs new file mode 100644 index 0000000000..6abd25d492 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Endpoint.cs @@ -0,0 +1,47 @@ +using Elsa.Abstractions; +using Elsa.Workflows.Runtime; +using JetBrains.Annotations; + +namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.GetCallStack; + +/// +/// Gets the call stack (execution chain) for a given activity execution. +/// Returns activity execution records from root to the specified activity, ordered by call stack depth. +/// Supports pagination for deep call stacks. +/// +[PublicAPI] +internal class Endpoint(IActivityExecutionStore store) : ElsaEndpoint +{ + /// + public override void Configure() + { + Get("/activity-executions/{id}/call-stack"); + ConfigurePermissions("read:activity-execution"); + } + + /// + public override async Task HandleAsync(Request request, CancellationToken cancellationToken) + { + var id = Route("id"); + var includeCrossWorkflowChain = request.IncludeCrossWorkflowChain ?? true; + var skip = request.Skip; + var take = request.Take; + + var result = await store.GetExecutionChainAsync(id, includeCrossWorkflowChain, skip, take, cancellationToken); + + if (result.TotalCount == 0) + { + await Send.NotFoundAsync(cancellationToken); + return; + } + + var response = new Response + { + ActivityExecutionId = id, + Items = result.Items.ToList(), + TotalCount = result.TotalCount + }; + + await Send.OkAsync(response, cancellationToken); + } +} diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Request.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Request.cs new file mode 100644 index 0000000000..d59ae78ec1 --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Request.cs @@ -0,0 +1,25 @@ +namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.GetCallStack; + +/// +/// Request for retrieving the call stack of an activity execution. +/// +public class Request +{ + /// + /// If true (default), includes parent workflow activities across workflow boundaries. + /// If false, stops at workflow boundaries. + /// + public bool? IncludeCrossWorkflowChain { get; set; } + + /// + /// The number of items to skip (for pagination). + /// Applied after ordering from root to leaf. + /// + public int? Skip { get; set; } + + /// + /// The maximum number of items to return (for pagination). + /// Recommended maximum: 100. + /// + public int? Take { get; set; } +} diff --git a/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Response.cs b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Response.cs new file mode 100644 index 0000000000..332950a2da --- /dev/null +++ b/src/modules/Elsa.Workflows.Api/Endpoints/ActivityExecutions/GetCallStack/Response.cs @@ -0,0 +1,25 @@ +using Elsa.Workflows.Runtime.Entities; + +namespace Elsa.Workflows.Api.Endpoints.ActivityExecutions.GetCallStack; + +/// +/// Response containing the call stack for an activity execution with pagination support. +/// +public class Response +{ + /// + /// The ID of the activity execution that was requested. + /// + public string ActivityExecutionId { get; set; } = null!; + + /// + /// The activity execution records in the call stack (ordered from root to current activity). + /// This may be a subset if pagination is applied. + /// + public ICollection Items { get; set; } = new List(); + + /// + /// The total number of items in the full call stack chain. + /// + public long TotalCount { get; set; } +} diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs index 06f3ec9df7..488e3f5237 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Counters.cs @@ -142,7 +142,7 @@ private async ValueTask ProcessChildCompletedAsync(ActivityExecutionContext flow var flowGraph = GetFlowGraph(flowchartContext); var flowScope = GetFlowScope(flowchartContext); var completedActivityExcecutedByBackwardConnection = completedActivityContext.ActivityInput.GetValueOrDefault(BackwardConnectionActivityInput); - bool hasScheduledActivity = await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, outcomes, OnChildCompletedAsync, completedActivityExcecutedByBackwardConnection); + bool hasScheduledActivity = await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, completedActivity, completedActivityContext, outcomes, OnChildCompletedAsync, completedActivityExcecutedByBackwardConnection); // If there are not any outbound connections, complete the flowchart activity if there is no other pending work if (!hasScheduledActivity) @@ -162,12 +162,13 @@ private async ValueTask ProcessChildCompletedAsync(ActivityExecutionContext flow /// Tracks activity and connection visits. /// The execution context of the flowchart. /// The current activity being processed. + /// The execution context of the completed activity (for call stack tracking). /// The outcomes that determine which connections were followed. /// The callback to invoke upon activity completion. - /// Indicates if the completed activity + /// Indicates if the completed activity /// was executed due to a backward connection. /// True if at least one activity was scheduled; otherwise, false. - private static async ValueTask MaybeScheduleOutboundActivitiesAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity activity, Outcomes outcomes, ActivityCompletionCallback completionCallback, bool completedActivityExecutedByBackwardConnection = false) + private static async ValueTask MaybeScheduleOutboundActivitiesAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity activity, ActivityExecutionContext? completedActivityContext, Outcomes outcomes, ActivityCompletionCallback completionCallback, bool completedActivityExecutedByBackwardConnection = false) { bool hasScheduledActivity = false; @@ -193,9 +194,9 @@ private static async ValueTask MaybeScheduleOutboundActivitiesAsync(FlowGr // Determine the scheduling strategy based on connection-type. if (flowGraph.IsBackwardConnection(outboundConnection, out var backwardConnectionIsValid)) // Backward connections are scheduled differently - hasScheduledActivity |= await MaybeScheduleBackwardConnectionActivityAsync(flowGraph, flowchartContext, outboundConnection, outboundActivity, connectionFollowed, backwardConnectionIsValid, completionCallback); + hasScheduledActivity |= await MaybeScheduleBackwardConnectionActivityAsync(flowGraph, flowchartContext, completedActivityContext, outboundConnection, outboundActivity, connectionFollowed, backwardConnectionIsValid, completionCallback); else - hasScheduledActivity |= await MaybeScheduleOutboundActivityAsync(flowGraph, flowScope, flowchartContext, outboundConnection, outboundActivity, completionCallback); + hasScheduledActivity |= await MaybeScheduleOutboundActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundConnection, outboundActivity, completionCallback); } return hasScheduledActivity; @@ -204,7 +205,7 @@ private static async ValueTask MaybeScheduleOutboundActivitiesAsync(FlowGr /// /// Schedules an outbound activity that originates from a backward connection. /// - private static async ValueTask MaybeScheduleBackwardConnectionActivityAsync(FlowGraph flowGraph, ActivityExecutionContext flowchartContext, Connection outboundConnection, IActivity outboundActivity, bool connectionFollowed, bool backwardConnectionIsValid, ActivityCompletionCallback completionCallback) + private static async ValueTask MaybeScheduleBackwardConnectionActivityAsync(FlowGraph flowGraph, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, Connection outboundConnection, IActivity outboundActivity, bool connectionFollowed, bool backwardConnectionIsValid, ActivityCompletionCallback completionCallback) { if (!connectionFollowed) { @@ -219,7 +220,8 @@ private static async ValueTask MaybeScheduleBackwardConnectionActivityAsyn var scheduleWorkOptions = new ScheduleWorkOptions { CompletionCallback = completionCallback, - Input = new Dictionary() { { BackwardConnectionActivityInput, true } } + Input = new Dictionary() { { BackwardConnectionActivityInput, true } }, + SchedulingActivityExecutionId = completedActivityContext?.Id }; await flowchartContext.ScheduleActivityAsync(outboundActivity, scheduleWorkOptions); @@ -247,15 +249,15 @@ private static async ValueTask GetMergeModeAsync(ActivityExecution /// /// Schedules a join activity based on inbound connection statuses. /// - private static async ValueTask MaybeScheduleOutboundActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, Connection outboundConnection, IActivity outboundActivity, ActivityCompletionCallback completionCallback) + private static async ValueTask MaybeScheduleOutboundActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, Connection outboundConnection, IActivity outboundActivity, ActivityCompletionCallback completionCallback) { FlowJoinMode mode = await GetMergeModeAsync(flowchartContext, outboundActivity); return mode switch { - FlowJoinMode.WaitAll => await MaybeScheduleWaitAllActivityAsync(flowGraph, flowScope, flowchartContext, outboundActivity, completionCallback), - FlowJoinMode.WaitAllActive => await MaybeScheduleWaitAllActiveActivityAsync(flowGraph, flowScope, flowchartContext, outboundActivity, completionCallback), - FlowJoinMode.WaitAny => await MaybeScheduleWaitAnyActivityAsync(flowGraph, flowScope, flowchartContext, outboundConnection, outboundActivity, completionCallback), + FlowJoinMode.WaitAll => await MaybeScheduleWaitAllActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundActivity, completionCallback), + FlowJoinMode.WaitAllActive => await MaybeScheduleWaitAllActiveActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundActivity, completionCallback), + FlowJoinMode.WaitAny => await MaybeScheduleWaitAnyActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundConnection, outboundActivity, completionCallback), _ => throw new($"Unsupported FlowJoinMode: {mode}"), }; } @@ -264,7 +266,7 @@ private static async ValueTask MaybeScheduleOutboundActivityAsync(FlowGrap /// Determines whether to schedule an activity based on the FlowJoinMode.WaitAll behavior. /// If all inbound connections were visited, it checks if they were all followed to decide whether to schedule or skip the activity. /// - private static async ValueTask MaybeScheduleWaitAllActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) + private static async ValueTask MaybeScheduleWaitAllActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) { if (!flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity)) // Not all inbound connections have been visited yet; do not schedule anything yet. @@ -272,17 +274,17 @@ private static async ValueTask MaybeScheduleWaitAllActivityAsync(FlowGraph if (flowScope.AllInboundConnectionsFollowed(flowGraph, outboundActivity)) // All inbound connections were followed; schedule the outbound activity. - return await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback); + return await ScheduleOutboundActivityAsync(flowchartContext, completedActivityContext, outboundActivity, completionCallback); else // No inbound connections were followed; skip the outbound activity. - return await SkipOutboundActivityAsync(flowGraph, flowScope, flowchartContext, outboundActivity, completionCallback); + return await SkipOutboundActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundActivity, completionCallback); } /// /// Determines whether to schedule an activity based on the FlowJoinMode.WaitAllActive behavior. /// If all inbound connections have been visited, it checks if any were followed to decide whether to schedule or skip the activity. /// - private static async ValueTask MaybeScheduleWaitAllActiveActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) + private static async ValueTask MaybeScheduleWaitAllActiveActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) { if (!flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity)) // Not all inbound connections have been visited yet; do not schedule anything yet. @@ -290,10 +292,10 @@ private static async ValueTask MaybeScheduleWaitAllActiveActivityAsync(Flo if (flowScope.AnyInboundConnectionsFollowed(flowGraph, outboundActivity)) // At least one inbound connection was followed; schedule the outbound activity. - return await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback); + return await ScheduleOutboundActivityAsync(flowchartContext, completedActivityContext, outboundActivity, completionCallback); else // No inbound connections were followed; skip the outbound activity. - return await SkipOutboundActivityAsync(flowGraph, flowScope, flowchartContext, outboundActivity, completionCallback); + return await SkipOutboundActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundActivity, completionCallback); } /// @@ -301,7 +303,7 @@ private static async ValueTask MaybeScheduleWaitAllActiveActivityAsync(Flo /// If any inbound connection has been followed, it schedules the activity and cancels remaining inbound activities. /// If a subsequent inbound connection is followed after the activity has been scheduled, it ignores it. /// - private static async ValueTask MaybeScheduleWaitAnyActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, Connection outboundConnection, IActivity outboundActivity, ActivityCompletionCallback completionCallback) + private static async ValueTask MaybeScheduleWaitAnyActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, Connection outboundConnection, IActivity outboundActivity, ActivityCompletionCallback completionCallback) { if (flowScope.ShouldIgnoreConnection(outboundConnection, outboundActivity)) // Ignore the connection if the outbound activity has already completed (JoinAny scenario) @@ -317,12 +319,12 @@ private static async ValueTask MaybeScheduleWaitAnyActivityAsync(FlowGraph await CancelRemainingInboundActivitiesAsync(flowchartContext, outboundActivity); // This is the first inbound connection followed; schedule the outbound activity - return await ScheduleOutboundActivityAsync(flowchartContext, outboundActivity, completionCallback); + return await ScheduleOutboundActivityAsync(flowchartContext, completedActivityContext, outboundActivity, completionCallback); } if (flowScope.AllInboundConnectionsVisited(flowGraph, outboundActivity)) // All inbound connections have been visited without any being followed; skip the outbound activity - return await SkipOutboundActivityAsync(flowGraph, flowScope, flowchartContext, outboundActivity, completionCallback); + return await SkipOutboundActivityAsync(flowGraph, flowScope, flowchartContext, completedActivityContext, outboundActivity, completionCallback); // No inbound connections have been followed yet; do not schedule anything yet. return false; @@ -331,18 +333,23 @@ private static async ValueTask MaybeScheduleWaitAnyActivityAsync(FlowGraph /// /// Schedules the outbound activity. /// - private static async ValueTask ScheduleOutboundActivityAsync(ActivityExecutionContext flowchartContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) + private static async ValueTask ScheduleOutboundActivityAsync(ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) { - await flowchartContext.ScheduleActivityAsync(outboundActivity, completionCallback); + var options = new ScheduleWorkOptions + { + CompletionCallback = completionCallback, + SchedulingActivityExecutionId = completedActivityContext?.Id + }; + await flowchartContext.ScheduleActivityAsync(outboundActivity, options); return true; } /// /// Skips the outbound activity by propagating skipped connections. /// - private static async ValueTask SkipOutboundActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) + private static async ValueTask SkipOutboundActivityAsync(FlowGraph flowGraph, FlowScope flowScope, ActivityExecutionContext flowchartContext, ActivityExecutionContext? completedActivityContext, IActivity outboundActivity, ActivityCompletionCallback completionCallback) { - return await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, outboundActivity, Outcomes.Empty, completionCallback); + return await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, outboundActivity, completedActivityContext, Outcomes.Empty, completionCallback); } private static async ValueTask CancelRemainingInboundActivitiesAsync(ActivityExecutionContext flowchartContext, IActivity outboundActivity) @@ -376,6 +383,6 @@ private async ValueTask OnCounterFlowActivityCanceledAsync(CancelSignal signal, var flowScope = flowchart.GetFlowScope(flowchartContext); // Propagate canceled connections visited count by scheduling with Outcomes.Empty - await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, context.SenderActivityExecutionContext.Activity, Outcomes.Empty, OnChildCompletedAsync); + await MaybeScheduleOutboundActivitiesAsync(flowGraph, flowScope, flowchartContext, context.SenderActivityExecutionContext.Activity, context.SenderActivityExecutionContext, Outcomes.Empty, OnChildCompletedAsync); } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs index 127fe1e0e1..163e0ce2b6 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Flowchart/Activities/Flowchart.Tokens.cs @@ -1,6 +1,7 @@ using Elsa.Extensions; using Elsa.Workflows.Activities.Flowchart.Extensions; using Elsa.Workflows.Activities.Flowchart.Models; +using Elsa.Workflows.Options; using Elsa.Workflows.Signals; namespace Elsa.Workflows.Activities.Flowchart.Activities; @@ -60,7 +61,12 @@ private async ValueTask OnChildCompletedTokenBasedLogicAsync(ActivityCompletedCo if (existingBlockedToken == null) { // Schedule the target. - await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync); + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedTokenBasedLogicAsync, + SchedulingActivityExecutionId = ctx.ChildContext.Id + }; + await flowContext.ScheduleActivityAsync(targetActivity, options); // Block other inbound connections (adjust per mode if needed). var otherInboundConnections = flowGraph.GetForwardInboundConnections(targetActivity) @@ -99,11 +105,23 @@ private async ValueTask OnChildCompletedTokenBasedLogicAsync(ActivityCompletedCo ); if (hasAllTokens) - await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync); + { + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedTokenBasedLogicAsync, + SchedulingActivityExecutionId = ctx.ChildContext.Id + }; + await flowContext.ScheduleActivityAsync(targetActivity, options); + } } else { - await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync); + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedTokenBasedLogicAsync, + SchedulingActivityExecutionId = ctx.ChildContext.Id + }; + await flowContext.ScheduleActivityAsync(targetActivity, options); } break; @@ -125,11 +143,23 @@ private async ValueTask OnChildCompletedTokenBasedLogicAsync(ActivityCompletedCo ); if (hasAllTokens) - await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync); + { + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedTokenBasedLogicAsync, + SchedulingActivityExecutionId = ctx.ChildContext.Id + }; + await flowContext.ScheduleActivityAsync(targetActivity, options); + } } else { - await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync); + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedTokenBasedLogicAsync, + SchedulingActivityExecutionId = ctx.ChildContext.Id + }; + await flowContext.ScheduleActivityAsync(targetActivity, options); } break; @@ -143,7 +173,14 @@ private async ValueTask OnChildCompletedTokenBasedLogicAsync(ActivityCompletedCo ); if (!hasUnconsumed) - await flowContext.ScheduleActivityAsync(targetActivity, OnChildCompletedTokenBasedLogicAsync); + { + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompletedTokenBasedLogicAsync, + SchedulingActivityExecutionId = ctx.ChildContext.Id + }; + await flowContext.ScheduleActivityAsync(targetActivity, options); + } break; } } diff --git a/src/modules/Elsa.Workflows.Core/Activities/Parallel.cs b/src/modules/Elsa.Workflows.Core/Activities/Parallel.cs index 61de5f867b..939c371590 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Parallel.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Parallel.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Elsa.Workflows.Attributes; +using Elsa.Workflows.Options; namespace Elsa.Workflows.Activities; @@ -36,8 +37,15 @@ protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContex context.SetProperty(ScheduledChildrenProperty, Activities.Count); + // For Parallel, all children are scheduled by the parent activity (this), so the scheduling activity is the Parallel itself + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompleted, + SchedulingActivityExecutionId = context.Id + }; + foreach (var activity in Activities) - await context.ScheduleActivityAsync(activity, OnChildCompleted); + await context.ScheduleActivityAsync(activity, options); } private static async ValueTask OnChildCompleted(ActivityCompletedContext context) diff --git a/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs b/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs index da758e276e..2043e49121 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/Sequence.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Elsa.Workflows.Attributes; +using Elsa.Workflows.Options; using Elsa.Workflows.Signals; using JetBrains.Annotations; @@ -29,7 +30,7 @@ protected override async ValueTask ScheduleChildrenAsync(ActivityExecutionContex await HandleItemAsync(context); } - private async ValueTask HandleItemAsync(ActivityExecutionContext context) + private async ValueTask HandleItemAsync(ActivityExecutionContext context, ActivityExecutionContext? completedChildContext = null) { var currentIndex = context.GetProperty(CurrentIndexProperty); var childActivities = Activities.ToList(); @@ -41,7 +42,12 @@ private async ValueTask HandleItemAsync(ActivityExecutionContext context) } var nextActivity = childActivities.ElementAt(currentIndex); - await context.ScheduleActivityAsync(nextActivity, OnChildCompleted); + var options = new ScheduleWorkOptions + { + CompletionCallback = OnChildCompleted, + SchedulingActivityExecutionId = completedChildContext?.Id + }; + await context.ScheduleActivityAsync(nextActivity, options); context.UpdateProperty(CurrentIndexProperty, x => x + 1); } @@ -59,7 +65,7 @@ private async ValueTask OnChildCompleted(ActivityCompletedContext context) return; } - await HandleItemAsync(targetContext); + await HandleItemAsync(targetContext, childContext); } private void OnBreakSignalReceived(BreakSignal signal, SignalContext signalContext) diff --git a/src/modules/Elsa.Workflows.Core/Activities/While.cs b/src/modules/Elsa.Workflows.Core/Activities/While.cs index 3dc28bab1e..0282aaa34c 100644 --- a/src/modules/Elsa.Workflows.Core/Activities/While.cs +++ b/src/modules/Elsa.Workflows.Core/Activities/While.cs @@ -4,6 +4,7 @@ using Elsa.Workflows.Attributes; using Elsa.Workflows.Behaviors; using Elsa.Workflows.Models; +using Elsa.Workflows.Options; using Elsa.Workflows.UIHints; using JetBrains.Annotations; @@ -83,16 +84,23 @@ public While(Func condition, IActivity? body = null, [CallerFilePath] stri private async ValueTask OnBodyCompleted(ActivityCompletedContext context) { - await HandleIterationAsync(context.TargetContext); + await HandleIterationAsync(context.TargetContext, context.ChildContext); } - private async ValueTask HandleIterationAsync(ActivityExecutionContext context) + private async ValueTask HandleIterationAsync(ActivityExecutionContext context, ActivityExecutionContext? completedChildContext = null) { var isBreaking = context.GetIsBreaking(); var loop = !isBreaking && await context.EvaluateInputPropertyAsync(x => x.Condition); if (loop) - await context.ScheduleActivityAsync(Body, OnBodyCompleted); + { + var options = new ScheduleWorkOptions + { + CompletionCallback = OnBodyCompleted, + SchedulingActivityExecutionId = completedChildContext?.Id + }; + await context.ScheduleActivityAsync(Body, options); + } else await context.CompleteActivityAsync(); } diff --git a/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs b/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs index b6cb4b17a2..4ac216eab3 100644 --- a/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs +++ b/src/modules/Elsa.Workflows.Core/Behaviors/ScheduledChildCallbackBehavior.cs @@ -1,5 +1,4 @@ using Elsa.Mediator.Contracts; -using Elsa.Workflows.Notifications; using JetBrains.Annotations; using ActivityCompleted = Elsa.Workflows.Signals.ActivityCompleted; @@ -29,11 +28,10 @@ private async ValueTask OnActivityCompletedAsync(ActivityCompleted signal, Signa var completedContext = new ActivityCompletedContext(activityExecutionContext, childActivityExecutionContext, signal.Result); var tag = callbackEntry.Tag; completedContext.TargetContext.Tag = tag; - + var mediator = activityExecutionContext.GetRequiredService(); var invokingActivityCallbackNotification = new Notifications.InvokingActivityCallback(activityExecutionContext, childActivityExecutionContext); await mediator.SendAsync(invokingActivityCallbackNotification, context.CancellationToken); - await callbackEntry.CompletionCallback(completedContext); } } diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index 96693d4e53..61a097324f 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -107,7 +107,8 @@ public ActivityExecutionContext( public WorkflowExecutionContext WorkflowExecutionContext { get; } /// - /// The parent activity execution context, if any. + /// The parent activity execution context, if any. + /// This represents the structural container activity (e.g., Flowchart contains all its children). /// public ActivityExecutionContext? ParentActivityExecutionContext { @@ -119,6 +120,27 @@ internal set } } + /// + /// The ID of the activity execution context that scheduled this activity execution. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity, + /// distinct from which represents the structural container. + /// For example, in a Flowchart, Activity B might complete and schedule Activity C, making B the scheduling activity for C. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The ID of the activity that scheduled this activity execution (denormalized for convenience). + /// This is the Activity.Id from the activity execution context identified by . + /// + public string? SchedulingActivityId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this activity execution. + /// This is set when crossing workflow boundaries (e.g., via ExecuteWorkflow or DispatchWorkflow). + /// For activities within the same workflow instance, this will be null. + /// + public string? SchedulingWorkflowInstanceId { get; set; } + /// /// The expression execution context. /// diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index b8b7d4f11d..5d74a4e2fc 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -350,6 +350,18 @@ public async Task SetWorkflowGraphAsync(WorkflowGraph workflowGraph) /// public IDictionary TransientProperties { get; set; } = new Dictionary(); + /// + /// The ambient scheduling activity execution ID. Used as a fallback when ScheduleWorkOptions does not explicitly specify a scheduling activity. + /// This is set automatically during completion callbacks, bookmark resumes, and child workflow starts. + /// + public string? CurrentSchedulingActivityExecutionId { get; set; } + + /// + /// The ambient scheduling workflow instance ID. Used as a fallback when crossing workflow boundaries. + /// This is set automatically when starting a child workflow to track the parent workflow instance. + /// + public string? CurrentSchedulingWorkflowInstanceId { get; set; } + /// /// A collection of incidents that may have occurred during execution. /// @@ -604,6 +616,20 @@ public async Task CreateActivityExecutionContextAsync( var activityInput = options?.Input ?? new Dictionary(StringComparer.OrdinalIgnoreCase); activityExecutionContext.ActivityInput.Merge(activityInput); + // Populate call stack fields from options + activityExecutionContext.SchedulingActivityExecutionId = options?.SchedulingActivityExecutionId; + activityExecutionContext.SchedulingWorkflowInstanceId = options?.SchedulingWorkflowInstanceId; + + // Denormalize the scheduling activity ID for convenience + if (options?.SchedulingActivityExecutionId != null) + { + var schedulingContext = ActivityExecutionContexts.FirstOrDefault(x => x.Id == options.SchedulingActivityExecutionId); + if (schedulingContext != null) + { + activityExecutionContext.SchedulingActivityId = schedulingContext.Activity.Id; + } + } + return activityExecutionContext; } diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 90524fe51c..3064575593 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -485,5 +485,37 @@ public void SetExtensionsMetadata(string key, object? value) public bool GetHasEvaluatedProperties() => activityExecutionContext.TransientProperties.TryGetValue("HasEvaluatedProperties", out var value) && value; public void SetHasEvaluatedProperties() => activityExecutionContext.TransientProperties["HasEvaluatedProperties"] = true; + + /// + /// Retrieves the complete execution chain for this activity execution context by traversing the SchedulingActivityExecutionId chain. + /// Returns contexts ordered from root (depth 0) to this context. + /// + /// A collection of activity execution contexts representing the complete call stack, ordered from root to leaf. + public IEnumerable GetExecutionChain() + { + var chain = new List(); + var currentContext = activityExecutionContext; + + // Traverse the chain backwards from this context to the root + while (currentContext != null) + { + chain.Add(currentContext); + + // Find the parent in the workflow execution context + if (currentContext.SchedulingActivityExecutionId != null) + { + currentContext = currentContext.WorkflowExecutionContext.ActivityExecutionContexts + .FirstOrDefault(x => x.Id == currentContext.SchedulingActivityExecutionId); + } + else + { + break; + } + } + + // Reverse to get root-to-leaf order + chain.Reverse(); + return chain; + } } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs index e90c28b6ff..e32bd633ea 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/WorkflowExecutionContextExtensions.cs @@ -17,10 +17,19 @@ public static class WorkflowExecutionContextExtensions /// /// Schedules the workflow for execution. /// - public ActivityWorkItem ScheduleWorkflow(IDictionary? input = null, IEnumerable? variables = null) + public ActivityWorkItem ScheduleWorkflow( + IDictionary? input = null, + IEnumerable? variables = null, + string? schedulingActivityExecutionId = null, + string? schedulingWorkflowInstanceId = null) { var workflow = workflowExecutionContext.Workflow; - var workItem = new ActivityWorkItem(workflow, input: input, variables: variables); + var workItem = new ActivityWorkItem( + workflow, + input: input, + variables: variables, + schedulingActivityExecutionId: schedulingActivityExecutionId, + schedulingWorkflowInstanceId: schedulingWorkflowInstanceId); workflowExecutionContext.Scheduler.Schedule(workItem); return workItem; } diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs index 79cac9b2c8..71081ab497 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/DefaultActivityInvokerMiddleware.cs @@ -106,9 +106,7 @@ public async ValueTask InvokeAsync(ActivityExecutionContext context) /// protected virtual async ValueTask ExecuteActivityAsync(ActivityExecutionContext context) { - var executeDelegate = context.WorkflowExecutionContext.ExecuteDelegate - ?? (ExecuteActivityDelegate)Delegate.CreateDelegate(typeof(ExecuteActivityDelegate), context.Activity, ExecuteAsyncMethodInfo); - + var executeDelegate = context.WorkflowExecutionContext.ExecuteDelegate ?? (ExecuteActivityDelegate)Delegate.CreateDelegate(typeof(ExecuteActivityDelegate), context.Activity, ExecuteAsyncMethodInfo); await executeDelegate(context); } diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs index f939d1e64a..7e31cd010a 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/DefaultActivitySchedulerMiddleware.cs @@ -55,7 +55,9 @@ private async Task ExecuteWorkItemAsync(WorkflowExecutionContext context, Activi ExistingActivityExecutionContext = workItem.ExistingActivityExecutionContext, Tag = workItem.Tag, Variables = workItem.Variables, - Input = workItem.Input + Input = workItem.Input, + SchedulingActivityExecutionId = workItem.SchedulingActivityExecutionId, + SchedulingWorkflowInstanceId = workItem.SchedulingWorkflowInstanceId }; await activityInvoker.InvokeAsync(context, workItem.Activity, options); diff --git a/src/modules/Elsa.Workflows.Core/Models/ActivityWorkItem.cs b/src/modules/Elsa.Workflows.Core/Models/ActivityWorkItem.cs index 8634787744..bae0a548cb 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ActivityWorkItem.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ActivityWorkItem.cs @@ -16,7 +16,9 @@ public ActivityWorkItem( object? tag = null, IEnumerable? variables = null, ActivityExecutionContext? existingActivityExecutionContext = null, - IDictionary? input = null) + IDictionary? input = null, + string? schedulingActivityExecutionId = null, + string? schedulingWorkflowInstanceId = null) { Activity = activity; Owner = owner; @@ -24,6 +26,8 @@ public ActivityWorkItem( Variables = variables; ExistingActivityExecutionContext = existingActivityExecutionContext; Input = input ?? new Dictionary(); + SchedulingActivityExecutionId = schedulingActivityExecutionId; + SchedulingWorkflowInstanceId = schedulingWorkflowInstanceId; } /// @@ -55,4 +59,18 @@ public ActivityWorkItem( /// Optional input to pass to the activity. /// public IDictionary Input { get; set; } + + /// + /// The ID of the activity execution context that scheduled this work item. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity, + /// distinct from the structural parent (). + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this work item. + /// This is set when crossing workflow boundaries (e.g., via ExecuteWorkflow or DispatchWorkflow). + /// For activities within the same workflow instance, this will be null. + /// + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/ScheduledActivityOptions.cs b/src/modules/Elsa.Workflows.Core/Models/ScheduledActivityOptions.cs index 403c91e652..ff432242ff 100644 --- a/src/modules/Elsa.Workflows.Core/Models/ScheduledActivityOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Models/ScheduledActivityOptions.cs @@ -10,4 +10,10 @@ public class ScheduledActivityOptions public string? ExistingActivityInstanceId { get; set; } public bool PreventDuplicateScheduling { get; set; } public IDictionary? Input { get; set; } + + /// + /// The ID of the activity execution context that scheduled this activity. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity. + /// + public string? SchedulingActivityExecutionId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Options/ActivityInvocationOptions.cs b/src/modules/Elsa.Workflows.Core/Options/ActivityInvocationOptions.cs index 5789dd4c47..8a0782c7b1 100644 --- a/src/modules/Elsa.Workflows.Core/Options/ActivityInvocationOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Options/ActivityInvocationOptions.cs @@ -49,4 +49,18 @@ public ActivityInvocationOptions( /// Optional input to pass to the activity. /// public IDictionary Input { get; set; } + + /// + /// The ID of the activity execution context that scheduled this invocation. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity, + /// distinct from the structural parent (). + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this invocation. + /// This is set when crossing workflow boundaries (e.g., via ExecuteWorkflow or DispatchWorkflow). + /// For activities within the same workflow instance, this will be null. + /// + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs b/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs index fa3ab47a37..f9e486226b 100644 --- a/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Options/RunWorkflowOptions.cs @@ -16,4 +16,16 @@ public class RunWorkflowOptions public IDictionary? Properties { get; set; } public string? TriggerActivityId { get; set; } public string? ParentWorkflowInstanceId { get; set; } + + /// + /// The ID of the activity execution context that scheduled this workflow execution (for cross-workflow call stack tracking). + /// This is set when a parent workflow invokes this workflow via ExecuteWorkflow or DispatchWorkflow. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the parent workflow that scheduled this workflow execution. + /// This is set when crossing workflow boundaries. + /// + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Options/ScheduleWorkOptions.cs b/src/modules/Elsa.Workflows.Core/Options/ScheduleWorkOptions.cs index caa0d932c1..917fc6c9eb 100644 --- a/src/modules/Elsa.Workflows.Core/Options/ScheduleWorkOptions.cs +++ b/src/modules/Elsa.Workflows.Core/Options/ScheduleWorkOptions.cs @@ -30,4 +30,18 @@ public class ScheduleWorkOptions /// Input to send to the workflow. /// public IDictionary? Input { get; set; } + + /// + /// The ID of the activity execution context that scheduled this work item. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity, + /// distinct from the structural parent ( or Owner). + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this work item. + /// This is set when crossing workflow boundaries (e.g., via ExecuteWorkflow or DispatchWorkflow). + /// For activities within the same workflow instance, this will be null. + /// + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Services/ActivityExecutionContextSchedulerStrategy.cs b/src/modules/Elsa.Workflows.Core/Services/ActivityExecutionContextSchedulerStrategy.cs index 25e8d2160b..c84b403539 100644 --- a/src/modules/Elsa.Workflows.Core/Services/ActivityExecutionContextSchedulerStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/Services/ActivityExecutionContextSchedulerStrategy.cs @@ -33,12 +33,13 @@ public async Task ScheduleActivityAsync(ActivityExecutionContext context, Activi Options = options != null ? new ScheduledActivityOptions { - CompletionCallback = options?.CompletionCallback?.Method.Name, - Tag = options?.Tag, - ExistingActivityInstanceId = options?.ExistingActivityExecutionContext?.Id, - PreventDuplicateScheduling = options?.PreventDuplicateScheduling ?? false, - Variables = options?.Variables?.ToList(), - Input = options?.Input + CompletionCallback = options.CompletionCallback?.Method.Name, + Tag = options.Tag, + ExistingActivityInstanceId = options.ExistingActivityExecutionContext?.Id, + PreventDuplicateScheduling = options.PreventDuplicateScheduling, + SchedulingActivityExecutionId = options.SchedulingActivityExecutionId ?? context.Id, + Variables = options.Variables?.ToList(), + Input = options.Input } : null }; diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowExecutionContextSchedulerStrategy.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowExecutionContextSchedulerStrategy.cs index 7142cbcd3d..90eb2dac77 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowExecutionContextSchedulerStrategy.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowExecutionContextSchedulerStrategy.cs @@ -26,7 +26,22 @@ public ActivityWorkItem Schedule(WorkflowExecutionContext context, ActivityNode var activity = activityNode.Activity; var tag = options?.Tag; - var workItem = new ActivityWorkItem(activity, owner, tag, options?.Variables, options?.ExistingActivityExecutionContext, options?.Input); + + // Use explicit SchedulingActivityExecutionId from options, or fall back to owner context. + var schedulingActivityExecutionId = options?.SchedulingActivityExecutionId ?? owner.Id; + + // Use explicit SchedulingWorkflowInstanceId from options, if any. + var schedulingWorkflowInstanceId = options?.SchedulingWorkflowInstanceId; + + var workItem = new ActivityWorkItem( + activity, + owner, + tag, + options?.Variables, + options?.ExistingActivityExecutionContext, + options?.Input, + schedulingActivityExecutionId, + schedulingWorkflowInstanceId); var completionCallback = options?.CompletionCallback; context.AddCompletionCallback(owner, activityNode, completionCallback, tag); diff --git a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs index 45e9ee0f2b..ffadd15eea 100644 --- a/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs +++ b/src/modules/Elsa.Workflows.Core/Services/WorkflowRunner.cs @@ -88,7 +88,9 @@ public async Task RunAsync(WorkflowGraph workflowGraph, RunWo cancellationToken); // Schedule the first activity. - workflowExecutionContext.ScheduleWorkflow(); + workflowExecutionContext.ScheduleWorkflow( + schedulingActivityExecutionId: options?.SchedulingActivityExecutionId, + schedulingWorkflowInstanceId: options?.SchedulingWorkflowInstanceId); return await RunAsync(workflowExecutionContext); } @@ -172,7 +174,13 @@ public async Task RunAsync(WorkflowGraph workflowGraph, Workf { // Nothing was scheduled. Schedule the workflow itself. var vars = variables?.Select(x => new Variable(x.Key, x.Value)).ToList(); - workflowExecutionContext.ScheduleWorkflow(variables: vars); + var schedulingActivityExecutionId = options?.SchedulingActivityExecutionId; + var schedulingWorkflowInstanceId = options?.SchedulingWorkflowInstanceId; + + workflowExecutionContext.ScheduleWorkflow( + variables: vars, + schedulingActivityExecutionId: schedulingActivityExecutionId, + schedulingWorkflowInstanceId: schedulingWorkflowInstanceId); } } diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs index 22046821f6..ba4941ee0a 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/DispatchWorkflow.cs @@ -133,6 +133,8 @@ private async ValueTask DispatchChildWorkflowAsync(ActivityExecutionCont Properties = properties, CorrelationId = correlationId, InstanceId = instanceId, + SchedulingActivityExecutionId = context.Id, + SchedulingWorkflowInstanceId = parentInstanceId }; var options = new DispatchWorkflowOptions { diff --git a/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs b/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs index 4fda5e8808..40237b6b12 100644 --- a/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs +++ b/src/modules/Elsa.Workflows.Runtime/Activities/ExecuteWorkflow.cs @@ -108,7 +108,9 @@ private async ValueTask ExecuteWorkflowAsync(ActivityExec Input = input, Properties = properties, CorrelationId = correlationId, - WorkflowInstanceId = identityGenerator.GenerateId() + WorkflowInstanceId = identityGenerator.GenerateId(), + SchedulingActivityExecutionId = context.Id, + SchedulingWorkflowInstanceId = parentInstanceId }; var workflowResult = await workflowInvoker.InvokeAsync(workflowGraph, options, context.CancellationToken); diff --git a/src/modules/Elsa.Workflows.Runtime/Commands/DispatchWorkflowDefinitionCommand.cs b/src/modules/Elsa.Workflows.Runtime/Commands/DispatchWorkflowDefinitionCommand.cs index aa5bdbbc25..72a2dec752 100644 --- a/src/modules/Elsa.Workflows.Runtime/Commands/DispatchWorkflowDefinitionCommand.cs +++ b/src/modules/Elsa.Workflows.Runtime/Commands/DispatchWorkflowDefinitionCommand.cs @@ -15,4 +15,6 @@ public class DispatchWorkflowDefinitionCommand(string definitionVersionId) : ICo public string? CorrelationId { get; set; } public string? InstanceId { get; set; } public string? TriggerActivityId { get; set; } + public string? SchedulingActivityExecutionId { get; set; } + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutionStore.cs b/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutionStore.cs index 4190275f8d..c8647f71ed 100644 --- a/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutionStore.cs +++ b/src/modules/Elsa.Workflows.Runtime/Contracts/IActivityExecutionStore.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Models; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.OrderDefinitions; @@ -78,4 +79,21 @@ public interface IActivityExecutionStore : ILogRecordStoreAn optional cancellation token. /// The number of deleted records. Task DeleteManyAsync(ActivityExecutionRecordFilter filter, CancellationToken cancellationToken = default); -} \ No newline at end of file + + /// + /// Retrieves the execution chain for the specified activity execution record by traversing the SchedulingActivityExecutionId chain. + /// Returns records ordered from root (depth 0) to the specified record. + /// + /// The ID of the activity execution record to retrieve the chain for. + /// If true (default), follows SchedulingWorkflowInstanceId across workflow boundaries. If false, stops at workflow boundaries. + /// The number of items to skip (for pagination). Applied after ordering from root to leaf. + /// The maximum number of items to return (for pagination). + /// An optional cancellation token. + /// A paginated result containing the call stack records, ordered from root to leaf. + Task> GetExecutionChainAsync( + string activityExecutionId, + bool includeCrossWorkflowChain = true, + int? skip = null, + int? take = null, + CancellationToken cancellationToken = default); +} diff --git a/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs b/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs index 2b6bd1e749..43d73a65f6 100644 --- a/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs +++ b/src/modules/Elsa.Workflows.Runtime/Entities/ActivityExecutionRecord.cs @@ -96,6 +96,29 @@ public partial class ActivityExecutionRecord : Entity, ILogRecord /// Gets or sets the time at which the activity execution completed. /// public DateTimeOffset? CompletedAt { get; set; } + + /// + /// The ID of the activity execution context that scheduled this activity execution. + /// This represents the temporal/execution predecessor that directly triggered execution of this activity. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The ID of the activity that scheduled this activity execution (denormalized for convenience). + /// + public string? SchedulingActivityId { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this activity execution. + /// This is set when crossing workflow boundaries (e.g., via ExecuteWorkflow or DispatchWorkflow). + /// + public string? SchedulingWorkflowInstanceId { get; set; } + + /// + /// The depth of this activity in the call stack (0 for root activities). + /// Calculated by traversing the SchedulingActivityExecutionId chain until reaching null. + /// + public int? CallStackDepth { get; set; } } public partial class ActivityExecutionRecord diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionRecordExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionRecordExtensions.cs new file mode 100644 index 0000000000..cc345b6744 --- /dev/null +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/ActivityExecutionRecordExtensions.cs @@ -0,0 +1,32 @@ +using Elsa.Common.Models; +using Elsa.Workflows.Runtime.Entities; + +namespace Elsa.Workflows.Runtime.Extensions; + +/// +/// Provides extension methods for . +/// +public static class ActivityExecutionRecordExtensions +{ + /// + /// Retrieves the execution chain for the specified activity execution record by using the store. + /// Returns records ordered from root (depth 0) to the specified record. + /// + /// The activity execution record to retrieve the chain for. + /// The activity execution store to use for retrieving the chain. + /// If true (default), follows SchedulingWorkflowInstanceId across workflow boundaries. + /// The number of items to skip (for pagination). + /// The maximum number of items to return (for pagination). + /// An optional cancellation token. + /// A paginated result containing the call stack records, ordered from root to leaf. + public static Task> GetExecutionChainAsync( + this ActivityExecutionRecord record, + IActivityExecutionStore store, + bool includeCrossWorkflowChain = true, + int? skip = null, + int? take = null, + CancellationToken cancellationToken = default) + { + return store.GetExecutionChainAsync(record.Id, includeCrossWorkflowChain, skip, take, cancellationToken); + } +} diff --git a/src/modules/Elsa.Workflows.Runtime/Filters/ActivityExecutionRecordFilter.cs b/src/modules/Elsa.Workflows.Runtime/Filters/ActivityExecutionRecordFilter.cs index 1526f5c6af..f8acfc32eb 100644 --- a/src/modules/Elsa.Workflows.Runtime/Filters/ActivityExecutionRecordFilter.cs +++ b/src/modules/Elsa.Workflows.Runtime/Filters/ActivityExecutionRecordFilter.cs @@ -72,6 +72,42 @@ public class ActivityExecutionRecordFilter /// public bool? Completed { get; set; } + /// + /// The ID of the activity execution context that scheduled this activity execution. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The IDs of the activity execution contexts that scheduled activity executions. + /// + public ICollection? SchedulingActivityExecutionIds { get; set; } + + /// + /// The ID of the activity that scheduled this activity execution (denormalized). + /// + public string? SchedulingActivityId { get; set; } + + /// + /// The IDs of the activities that scheduled activity executions (denormalized). + /// + public ICollection? SchedulingActivityIds { get; set; } + + /// + /// The workflow instance ID of the workflow that scheduled this activity execution. + /// Used for cross-workflow call stack tracking. + /// + public string? SchedulingWorkflowInstanceId { get; set; } + + /// + /// The workflow instance IDs of the workflows that scheduled activity executions. + /// + public ICollection? SchedulingWorkflowInstanceIds { get; set; } + + /// + /// The call stack depth of the activity execution. + /// + public int? CallStackDepth { get; set; } + /// /// Returns true if the filter is empty. /// @@ -88,7 +124,14 @@ public class ActivityExecutionRecordFilter && Names == null && Status == null && Statuses == null - && Completed == null; + && Completed == null + && SchedulingActivityExecutionId == null + && SchedulingActivityExecutionIds == null + && SchedulingActivityId == null + && SchedulingActivityIds == null + && SchedulingWorkflowInstanceId == null + && SchedulingWorkflowInstanceIds == null + && CallStackDepth == null; /// /// Applies the filter to the specified queryable. @@ -109,6 +152,13 @@ public IQueryable Apply(IQueryable x.Status == filter.Status); if (filter.Statuses != null && filter.Statuses.Any()) queryable = queryable.Where(x => filter.Statuses.Contains(x.Status)); if (filter.Completed != null) queryable = filter.Completed == true ? queryable.Where(x => x.CompletedAt != null) : queryable.Where(x => x.CompletedAt == null); + if (filter.SchedulingActivityExecutionId != null) queryable = queryable.Where(x => x.SchedulingActivityExecutionId == filter.SchedulingActivityExecutionId); + if (filter.SchedulingActivityExecutionIds != null && filter.SchedulingActivityExecutionIds.Any()) queryable = queryable.Where(x => x.SchedulingActivityExecutionId != null && filter.SchedulingActivityExecutionIds.Contains(x.SchedulingActivityExecutionId)); + if (filter.SchedulingActivityId != null) queryable = queryable.Where(x => x.SchedulingActivityId == filter.SchedulingActivityId); + if (filter.SchedulingActivityIds != null && filter.SchedulingActivityIds.Any()) queryable = queryable.Where(x => x.SchedulingActivityId != null && filter.SchedulingActivityIds.Contains(x.SchedulingActivityId)); + if (filter.SchedulingWorkflowInstanceId != null) queryable = queryable.Where(x => x.SchedulingWorkflowInstanceId == filter.SchedulingWorkflowInstanceId); + if (filter.SchedulingWorkflowInstanceIds != null && filter.SchedulingWorkflowInstanceIds.Any()) queryable = queryable.Where(x => x.SchedulingWorkflowInstanceId != null && filter.SchedulingWorkflowInstanceIds.Contains(x.SchedulingWorkflowInstanceId)); + if (filter.CallStackDepth != null) queryable = queryable.Where(x => x.CallStackDepth == filter.CallStackDepth); return queryable; } diff --git a/src/modules/Elsa.Workflows.Runtime/Handlers/DispatchWorkflowRequestHandler.cs b/src/modules/Elsa.Workflows.Runtime/Handlers/DispatchWorkflowRequestHandler.cs index 04548af75b..e4880729c3 100644 --- a/src/modules/Elsa.Workflows.Runtime/Handlers/DispatchWorkflowRequestHandler.cs +++ b/src/modules/Elsa.Workflows.Runtime/Handlers/DispatchWorkflowRequestHandler.cs @@ -42,7 +42,9 @@ public virtual async Task HandleAsync(DispatchWorkflowDefinitionCommand co Input = command.Input, Properties = command.Properties, ParentId = command.ParentWorkflowInstanceId, - TriggerActivityId = command.TriggerActivityId + TriggerActivityId = command.TriggerActivityId, + SchedulingActivityExecutionId = command.SchedulingActivityExecutionId, + SchedulingWorkflowInstanceId = command.SchedulingWorkflowInstanceId }; await client.CreateAndRunInstanceAsync(createRequest, cancellationToken); return Unit.Instance; diff --git a/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs b/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs index 2a88511cf6..ae4e66b5b4 100644 --- a/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Messages/CreateAndRunWorkflowInstanceRequest.cs @@ -58,4 +58,16 @@ public class CreateAndRunWorkflowInstanceRequest /// When set to true, include workflow output in the response. /// public bool IncludeWorkflowOutput { get; set; } + + /// + /// The ID of the activity execution context that scheduled this workflow execution (for cross-workflow call stack tracking). + /// This is set when a parent workflow invokes this workflow via ExecuteWorkflow or DispatchWorkflow. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the parent workflow that scheduled this workflow execution. + /// This is set when crossing workflow boundaries. + /// + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceRequest.cs b/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceRequest.cs index 41745a3fd2..5dd9346399 100644 --- a/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Messages/RunWorkflowInstanceRequest.cs @@ -44,6 +44,18 @@ public class RunWorkflowInstanceRequest /// public bool IncludeWorkflowOutput { get; set; } + /// + /// The ID of the activity execution context that scheduled this workflow execution (for cross-workflow call stack tracking). + /// This is set when a parent workflow invokes this workflow via ExecuteWorkflow or DispatchWorkflow. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the parent workflow that scheduled this workflow execution. + /// This is set when crossing workflow boundaries. + /// + public string? SchedulingWorkflowInstanceId { get; set; } + /// /// Represents an empty object used as a default value. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Requests/DispatchWorkflowDefinitionRequest.cs b/src/modules/Elsa.Workflows.Runtime/Requests/DispatchWorkflowDefinitionRequest.cs index f007ed1f76..adb3af8d0c 100644 --- a/src/modules/Elsa.Workflows.Runtime/Requests/DispatchWorkflowDefinitionRequest.cs +++ b/src/modules/Elsa.Workflows.Runtime/Requests/DispatchWorkflowDefinitionRequest.cs @@ -58,4 +58,16 @@ public DispatchWorkflowDefinitionRequest(string definitionVersionId) /// The ID of the activity that triggered the workflow. /// public string? TriggerActivityId { get; set; } + + /// + /// The ID of the activity execution context that scheduled this workflow execution (for cross-workflow call stack tracking). + /// This is set when a parent workflow invokes this workflow via DispatchWorkflow. + /// + public string? SchedulingActivityExecutionId { get; set; } + + /// + /// The workflow instance ID of the parent workflow that scheduled this workflow execution. + /// This is set when crossing workflow boundaries. + /// + public string? SchedulingWorkflowInstanceId { get; set; } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs index b5b99007f6..7b8f8587bd 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/BackgroundWorkflowDispatcher.cs @@ -28,6 +28,8 @@ public async Task DispatchAsync(DispatchWorkflowDefini InstanceId = request.InstanceId, TriggerActivityId = request.TriggerActivityId, ParentWorkflowInstanceId = request.ParentWorkflowInstanceId, + SchedulingActivityExecutionId = request.SchedulingActivityExecutionId, + SchedulingWorkflowInstanceId = request.SchedulingWorkflowInstanceId }; await commandSender.SendAsync(command, CommandStrategy.Background, CreateHeaders(), cancellationToken); diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs index 223cd55f34..f00299fe2a 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultActivityExecutionMapper.cs @@ -28,6 +28,9 @@ public async Task MapAsync(ActivityExecutionContext sou var persistableJournalData = GetPersistableDictionary(source.JournalData!, persistenceMap.InternalState); var cancellationToken = source.CancellationToken; + // Calculate call stack depth by traversing the scheduling chain + var callStackDepth = CalculateCallStackDepth(source); + var record = new ActivityExecutionRecord { Id = source.Id, @@ -47,7 +50,11 @@ public async Task MapAsync(ActivityExecutionContext sou HasBookmarks = source.Bookmarks.Any(), Status = source.Status, AggregateFaultCount = source.AggregateFaultCount, - CompletedAt = source.CompletedAt + CompletedAt = source.CompletedAt, + SchedulingActivityExecutionId = source.SchedulingActivityExecutionId, + SchedulingActivityId = source.SchedulingActivityId, + SchedulingWorkflowInstanceId = source.SchedulingWorkflowInstanceId, + CallStackDepth = callStackDepth }; record = record.SanitizeLogMessage(); @@ -100,4 +107,32 @@ record = record.SanitizeLogMessage(); { return mode == LogPersistenceMode.Include ? new Dictionary(dictionary) : null; } + + private int? CalculateCallStackDepth(ActivityExecutionContext source) + { + if (source.SchedulingActivityExecutionId == null) + return 0; // Root activity + + var depth = 1; + var currentSchedulingId = source.SchedulingActivityExecutionId; + var workflowExecutionContext = source.WorkflowExecutionContext; + + // Traverse the scheduling chain until we reach a root activity (null scheduling ID) + // Limit depth to prevent infinite loops in case of circular references + const int maxDepth = 1000; + + while (currentSchedulingId != null && depth < maxDepth) + { + var schedulingContext = workflowExecutionContext.ActivityExecutionContexts + .FirstOrDefault(x => x.Id == currentSchedulingId); + + if (schedulingContext == null) + break; // Can't traverse further if the scheduling context isn't in the current workflow + + currentSchedulingId = schedulingContext.SchedulingActivityExecutionId; + depth++; + } + + return depth; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs index 75bcfb1183..af9c10bb6e 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/LocalWorkflowClient.cs @@ -75,7 +75,9 @@ public async Task CreateAndRunInstanceAsync(CreateA Properties = request.Properties, TriggerActivityId = request.TriggerActivityId, ActivityHandle = request.ActivityHandle, - IncludeWorkflowOutput = request.IncludeWorkflowOutput + IncludeWorkflowOutput = request.IncludeWorkflowOutput, + SchedulingActivityExecutionId = request.SchedulingActivityExecutionId, + SchedulingWorkflowInstanceId = request.SchedulingWorkflowInstanceId }, cancellationToken); } @@ -89,7 +91,11 @@ public async Task CancelAsync(CancellationToken cancellationToken = default) private async Task CancelAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) { if (workflowInstance.Status != WorkflowStatus.Running) return; - var workflowGraph = await GetWorkflowGraphAsync(workflowInstance, cancellationToken); + var workflowGraph = await TryGetWorkflowGraphAsync(workflowInstance, cancellationToken); + + if (workflowGraph == null) + return; + var workflowState = await workflowCanceler.CancelWorkflowAsync(workflowGraph, workflowInstance.WorkflowState, cancellationToken); await workflowInstanceManager.SaveAsync(workflowState, cancellationToken); } @@ -152,6 +158,8 @@ public async Task RunInstanceAsync(WorkflowInstance BookmarkId = request.BookmarkId, TriggerActivityId = request.TriggerActivityId, ActivityHandle = request.ActivityHandle, + SchedulingActivityExecutionId = request.SchedulingActivityExecutionId, + SchedulingWorkflowInstanceId = request.SchedulingWorkflowInstanceId }; var workflowGraph = await GetWorkflowGraphAsync(workflowInstance, cancellationToken); @@ -213,4 +221,28 @@ private async Task GetWorkflowGraphAsync(WorkflowDefinitionHandle if (!result.WorkflowGraphExists) throw new WorkflowMaterializerNotFoundException(result.WorkflowDefinition!.MaterializerName); return result.WorkflowGraph!; } + + private async Task TryGetWorkflowGraphAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken) + { + var handle = WorkflowDefinitionHandle.ByDefinitionVersionId(workflowInstance.DefinitionVersionId); + return await TryGetWorkflowGraphAsync(handle, cancellationToken); + } + + private async Task TryGetWorkflowGraphAsync(WorkflowDefinitionHandle definitionHandle, CancellationToken cancellationToken) + { + var result = await workflowDefinitionService.TryFindWorkflowGraphAsync(definitionHandle, cancellationToken); + if (!result.WorkflowDefinitionExists) + { + logger.LogWarning("Workflow definition not found: {WorkflowDefinitionId}", definitionHandle.DefinitionId); + return null; + } + + if (!result.WorkflowGraphExists) + { + logger.LogWarning("Workflow materializer not found: {WorkflowDefinitionId}", definitionHandle.DefinitionId); + return null; + } + + return result.WorkflowGraph!; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Stores/MemoryActivityExecutionStore.cs b/src/modules/Elsa.Workflows.Runtime/Stores/MemoryActivityExecutionStore.cs index e09b0c5910..1c2c4a3d44 100644 --- a/src/modules/Elsa.Workflows.Runtime/Stores/MemoryActivityExecutionStore.cs +++ b/src/modules/Elsa.Workflows.Runtime/Stores/MemoryActivityExecutionStore.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Models; using Elsa.Common.Services; using Elsa.Extensions; using Elsa.Workflows.Runtime.Entities; @@ -92,5 +93,46 @@ public Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Cancella return Task.FromResult(records.LongCount()); } + /// + public 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 record = _store.Query(query => query.Where(x => x.Id == currentId)).FirstOrDefault(); + 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 Task.FromResult(Page.Of(chain, totalCount)); + } + private static IQueryable Filter(IQueryable queryable, ActivityExecutionRecordFilter filter) => filter.Apply(queryable); -} \ No newline at end of file +} diff --git a/src/modules/Elsa.Workflows.Runtime/Stores/NoopActivityExecutionStore.cs b/src/modules/Elsa.Workflows.Runtime/Stores/NoopActivityExecutionStore.cs index f371cc248c..68b027c75f 100644 --- a/src/modules/Elsa.Workflows.Runtime/Stores/NoopActivityExecutionStore.cs +++ b/src/modules/Elsa.Workflows.Runtime/Stores/NoopActivityExecutionStore.cs @@ -1,3 +1,4 @@ +using Elsa.Common.Models; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Filters; using Elsa.Workflows.Runtime.OrderDefinitions; @@ -55,4 +56,14 @@ public Task DeleteManyAsync(ActivityExecutionRecordFilter filter, Cancella { return Task.FromResult(0L); } -} \ No newline at end of file + + public Task> GetExecutionChainAsync( + string activityExecutionId, + bool includeCrossWorkflowChain = true, + int? skip = null, + int? take = null, + CancellationToken cancellationToken = default) + { + return Task.FromResult(Page.Empty()); + } +}