.NET: Align WorkflowOutputEvent streaming deserialization with SourceId to ExecutorId rename#6896
.NET: Align WorkflowOutputEvent streaming deserialization with SourceId to ExecutorId rename#6896kshyju wants to merge 5 commits into
Conversation
The WorkflowOutputEvent.SourceId property was renamed to ExecutorId, but DeserializeEventByType still looked for "sourceId" in the JSON payload. This caused a KeyNotFoundException when WatchStreamAsync encountered a WorkflowOutputEvent during streaming. Update the manual deserialization to read "executorId" instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Covers the serialize-deserialize path through DeserializeEventByType for WorkflowOutputEvent, which was missing and allowed the sourceId to executorId rename regression to go undetected. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes a runtime crash in durable workflow streaming by aligning the manual WorkflowOutputEvent JSON deserialization with the post-#3441 rename from SourceId to ExecutorId.
Changes:
- Update
DeserializeEventByTypeto readexecutorIdforWorkflowOutputEventdeserialization. - Add a unit test that validates
WorkflowOutputEventround-trips throughWatchStreamAsyncwithout throwing.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs | Updates the manual streaming deserialization path for WorkflowOutputEvent to match the new JSON key (executorId). |
| dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableStreamingWorkflowRunTests.cs | Adds coverage to ensure WorkflowOutputEvent can be streamed/deserialized correctly via WatchStreamAsync. |
Use TryGetProperty with sourceId fallback for backward compatibility with older persisted streams. Add CHANGELOG entry per repo convention. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add test covering the TryGetProperty fallback path for legacy sourceId payloads. Add PR link to CHANGELOG entry to match repo convention. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Rename legacyEventJson to LegacyEventJson to satisfy IDE1006 naming rule requiring constants to use PascalCase. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 94%
✓ Correctness
The fix is correct and well-tested. It replaces the crash-prone
GetProperty("sourceId")call withTryGetProperty("executorId")and a fallback toTryGetProperty("sourceId"), matching the currentWorkflowOutputEventclass whereExecutorIdis the primary property (serialized asexecutorId) andSourceIdis an[Obsolete]+[JsonIgnore]alias. The CHANGELOG entry follows the project convention with a PR link. Both the new-format and legacy-format deserialization paths are covered by tests. No correctness issues found.
✓ Security Reliability
The fix correctly replaces a crash-prone
GetProperty("sourceId")call with a robustTryGetPropertyfallback chain that handles both the newexecutorIdand legacysourceIdJSON keys. The CHANGELOG is properly formatted with a PR link. Tests cover both the modern and legacy deserialization paths. No security or reliability issues are introduced by this change.
✓ Test Coverage
The PR has good test coverage for the fix. Two new tests exercise both the modern
executorIdJSON path and the legacysourceIdfallback path. Assertions are meaningful — they verify the deserializedExecutorId,Data, andTagsproperties. The only untested edge case is when neither key is present (defaults to empty string), but this is a minor defensive path for corrupted payloads and not worth blocking on. The pre-existing gap wheretagsare not deserialized in the manual path is not introduced by this PR.
✓ Failure Modes
The PR correctly fixes a runtime crash in WatchStreamAsync caused by the sourceId→executorId rename. The use of TryGetProperty with fallback is the right approach since the surrounding catch block only handles JsonException, not the KeyNotFoundException that GetProperty would throw for missing keys. Tests cover both the new and legacy deserialization paths. CHANGELOG entry matches project conventions. No new failure modes introduced.
✓ Design Approach
I found one design issue in the new
WorkflowOutputEventfallback: it preserves backward compatibility forsourceId, but it also changes malformed payload handling from "reject as unparsable" to "silently construct an event with an empty executor ID." That weakens the event contract and can hide corrupted stream data. I did not find other design-level issues in the diff.
Automated review by kshyju's agents
| ? execIdElem.GetString() ?? string.Empty | ||
| : root.TryGetProperty("sourceId", out JsonElement srcIdElem) | ||
| ? srcIdElem.GetString() ?? string.Empty | ||
| : string.Empty; |
There was a problem hiding this comment.
This fallback now silently accepts a malformed payload like {"data":"x"} and produces a WorkflowOutputEvent with an empty ExecutorId. That violates the documented invariant that ExecutorId is "The unique identifier of the executor that yielded this output." The surrounding caller already treats JsonException as an unparsable event (DurableStreamingWorkflowRun.cs:395-397), so the backward-compat path should still fail closed when neither key is present instead of manufacturing anonymous output event.
| : string.Empty; | |
| string outputExecutorId = root.TryGetProperty("executorId", out JsonElement execIdElem) | |
| ? execIdElem.GetString() ?? string.Empty | |
| : root.TryGetProperty("sourceId", out JsonElement srcIdElem) | |
| ? srcIdElem.GetString() ?? string.Empty | |
| : throw new JsonException("WorkflowOutputEvent is missing required executorId/sourceId."); |
Motivation & Context
PR #3441 renamed
WorkflowOutputEvent.SourceIdtoExecutorId(keepingSourceIdas an[Obsolete]+[JsonIgnore]alias), but the manual streaming deserialization inDeserializeEventByTypewas not updated. It still callsroot.GetProperty("sourceId"), which throws aKeyNotFoundExceptionat runtime because the serialized JSON contains"executorId".This causes
WatchStreamAsyncto crash with an unhandled exception whenever the stream includes aWorkflowOutputEvent(e.g., when an executor callsYieldOutputAsync).Description & Review Guide
What are the major changes?
Updated the
WorkflowOutputEventbranch inDeserializeEventByTypeto read"executorId"from the JSON payload instead of"sourceId". Used variable nameoutputExecutorIdto avoid shadowing theexecutorIdvariable declared in theExecutorInvokedEvent/ExecutorCompletedEventbranches above.What is the impact of these changes?
Fixes a runtime crash in
WatchStreamAsyncfor any durable workflow that yields output. No behavioral change beyond fixing the deserialization to match the current property name.What do you want reviewers to focus on?
Confirming there are no other references to the old
"sourceId"JSON key in the durable task serialization/deserialization paths.Related Issue
Contribution Checklist