elsa-workflows / elsa-workflows/elsa-core

Switch throws on resume from JSON-serialized state

Open
#7,443 0 comments 1 reaction 2 assignees Claimed by @sfmskywalker View on GitHub
triaged
Dominant language
C#
Stars
7.9k
Forks
1.5k
Avg merge
15h 22m
Merged PRs (30d)
114

Description

## Description

`Switch.OnChildActivityCompletedAsync` throws `InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List´1[System.String]' to type 'System.Collections.Generic.HashSet´1[System.String]'.` whenever a child branch of a `Switch` activity suspends and the workflow is then resumed.

Root cause (introduced by #7151 / commit `fbe9ca64f`):

- In `ExecuteAsync`, a `HashSet` of scheduled child activity IDs is written to the activity execution context property bag:
```csharp
// src/modules/Elsa.Workflows.Core/Activities/Switch.cs (3.6.0/3.6.1)
var scheduledActivityIds = new HashSet();
// ...populated...
context.SetProperty("ScheduledActivityIds", scheduledActivityIds);
```
- In `OnChildActivityCompletedAsync`, the same key is read back, strongly typed as `HashSet`:
```csharp
var scheduledActivityIds = context.TargetContext.GetProperty>("ScheduledActivityIds");
```

If the workflow does not suspend between these two calls, this works because the original CLR instance is still in memory. If the workflow *does* suspend (e.g. a descendant activity creates a bookmark — `ShowDialog`, `Event`, a background activity, a Form, etc.), the activity execution context properties are serialized to JSON as part of `WorkflowState`. A `HashSet` round-trips through JSON as a plain array and is rehydrated as `List`. The subsequent cast to `HashSet` then fails on resume.

The result is that any `Switch` whose chosen branch contains a suspending child activity is broken on resume.

## Steps to Reproduce

1. Build a workflow with this shape:
```
Flowchart
└── Switch (one case with condition = true)
└──
└──
```
2. Start the workflow. It executes up to the suspending child and suspends as expected. `WorkflowState` is persisted.
3. Resume the workflow (deliver the event / signal the bookmark).
4. When the resumed child completes and the completion bubbles up to the `Switch`'s `OnChildActivityCompletedAsync` callback, `GetProperty>("ScheduledActivityIds")` throws `InvalidCastException`.

**Reproduction rate**: every time, given the shape above and any persistence backend that round-trips workflow state through JSON.

## Expected Behavior

`Switch` correctly tracks its remaining scheduled children across suspension/resume and completes when the last one finishes.

## Actual Behavior

```
System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List`1[System.String]' to type 'System.Collections.Generic.HashSet`1[System.String]'.
at Elsa.Extensions.DictionaryExtensions.TryGetValue[T](IDictionary`2 dictionary, String key, T& value)
at Elsa.Workflows.ActivityExecutionContext.GetProperty[T](String key)
at Elsa.Workflows.Activities.Switch.OnChildActivityCompletedAsync(ActivityCompletedContext context)
at Elsa.Workflows.Behaviors.ScheduledChildCallbackBehavior.OnActivityCompletedAsync(ActivityCompleted signal, SignalContext context)
...
at Elsa.Workflows.ActivityExecutionContext.CompleteActivityAsync(Object result)
at Elsa.Workflows.Activities.Flowchart.Activities.Flowchart.OnChildCompletedTokenBasedLogicAsync(ActivityCompletedContext ctx)
...
```

The `Switch` never completes; the workflow is left in an inconsistent state.

## Environment

- **Elsa Package Version**: 3.6.0 (also present in 3.6.1 — same `Switch.cs`; `git tag --contains fbe9ca64f` lists both).
- **Persistence**: EF Core SQL Server (any persistence provider that JSON-serializes `WorkflowState` should reproduce).

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.