elsa-workflows / elsa-workflows/elsa-core
Switch throws on resume from JSON-serialized state
- 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
Assessment
This issue has not been assessed yet.