Skip to content

.NET: Add support for AddSwitch in the durable workflow runner#6749

Open
kshyju wants to merge 7 commits into
mainfrom
shkr/6722
Open

.NET: Add support for AddSwitch in the durable workflow runner#6749
kshyju wants to merge 7 commits into
mainfrom
shkr/6722

Conversation

@kshyju

@kshyju kshyju commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

This PR adds support for AddSwitch (and target-selecting fan-out edges) in the durable workflow runner. A switch lets a workflow route a message to the first matching case among several branches, or to a default branch when none match, which is a natural fit for multi-way routing such as approval flows.

AddSwitch is closely related to AddEdge(..., condition:), which the durable runner already supports. The difference is in how each models branching:

  • AddEdge with a condition attaches an independent boolean to each edge. Every edge whose condition is true is traversed, so multiple branches can run.
  • AddSwitch models mutually exclusive, first-match-wins routing with a single default, so exactly one branch runs. Under the hood it compiles to a single fan-out edge whose EdgeAssigner selects the matching case's target(s).

The durable graph analyzer already read the per-edge condition on DirectEdgeData but did not carry the EdgeAssigner from FanOutEdgeData, so a switch lost its case selection and forwarded the message to every target instead of the selected one. This change threads the assigner through the durable routing layer so case selection is honored, matching the in-process runtime's behavior.

Description & Review Guide

  • What are the major changes?

    • WorkflowAnalyzer now captures the EdgeAssigner and SinkIds from FanOutEdgeData into a new WorkflowGraphInfo.FanOutRoutings map.
    • DurableEdgeMap builds a selector-aware DurableFanOutEdgeRouter when an assigner is present, wrapping one direct router per ordered sink.
    • DurableFanOutEdgeRouter evaluates the assigner against the deserialized message and routes to only the selected targets, mirroring the
      in-process FanOutEdgeRunner. With no assigner it keeps the existing forward-to-all behavior.
    • Deduplicated the message-deserialization helper shared by the direct and fan-out routers into DurableSerialization.DeserializeMessage.
    • Added source-generated log entries (EventIds 114/115) for selector matches and evaluation failures.
    • Added a new 09_SwitchRouting console sample (expense-approval routing) plus its samples-validation integration test.
  • What is the impact of these changes?

    • Switches and target-selecting fan-out edges now route to the correct branch(es) under the durable runner, matching in-process behavior. Plain fan-out (no selector) is unchanged. No public API changes, and this is not a breaking change.
  • What do you want reviewers to focus on?

    • The selection logic in DurableFanOutEdgeRouter.RouteMessage and the assigner/sink ordering in DurableEdgeMap, i.e. that the durable path faithfully mirrors the in-process FanOutEdgeRunner.

Related Issue

Fixes #6722

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI review requested due to automatic review settings June 25, 2026 18:01
@moonbox3 moonbox3 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net labels Jun 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds durable-workflow runner support for AddSwitch / selector-based fan-out routing by propagating FanOutEdgeData.EdgeAssigner through durable graph analysis and routing, plus tests and a new console sample demonstrating multi-way routing.

Changes:

  • Capture fan-out selector metadata (EdgeAssigner + ordered SinkIds) during workflow analysis and expose it via WorkflowGraphInfo.
  • Build a selector-aware durable fan-out router that evaluates the assigner and routes only to selected targets.
  • Add unit + integration coverage and a new 09_SwitchRouting durable console sample.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowGraphInfo.cs Adds FanOutRoutings to carry selector routing metadata into the durable runtime.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowAnalyzer.cs Captures FanOutEdgeData.EdgeAssigner and sink ordering into FanOutRoutings.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/DurableEdgeMap.cs Uses captured routing to construct a selector-aware durable fan-out router.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/DurableFanOutEdgeRouter.cs Implements selector evaluation and selective routing for fan-out edges.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/DurableDirectEdgeRouter.cs Deduplicates message deserialization by calling DurableSerialization.DeserializeMessage.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableSerialization.cs Introduces shared message deserialization helper with trimming/AOT suppressions.
dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs Adds log events for selector matches and selector evaluation failures.
dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/DurableEdgeMapSwitchTests.cs New unit tests validating switch/default behavior and non-selector fan-out behavior.
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/WorkflowConsoleAppSamplesValidation.cs Adds end-to-end console sample validation for switch routing.
dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/09_SwitchRouting/README.md Documents the new switch-routing sample and contrasts it with conditional edges.
dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/09_SwitchRouting/Program.cs Console app wiring a durable workflow using AddSwitch.
dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/09_SwitchRouting/Executors.cs Executors used by the sample (parse expense, route to approval path).
dotnet/samples/04-hosting/DurableWorkflows/ConsoleApps/09_SwitchRouting/09_SwitchRouting.csproj New sample project definition and references.
dotnet/agent-framework-dotnet.slnx Adds the new sample project to the solution.

Comment thread dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowAnalyzer.cs Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 87%

✓ Correctness

The PR correctly implements AddSwitch support in the durable workflow runner. The WorkflowAnalyzer now captures EdgeAssigner and SinkIds from FanOutEdgeData, DurableEdgeMap builds a selector-aware DurableFanOutEdgeRouter when an assigner is present, and the router faithfully mirrors the in-process FanOutEdgeRunner's evaluation logic (calling the assigner with the deserialized message and target count, then routing to selected indices). The deserialization helper is cleanly deduplicated, error handling is appropriate for a durable context, and bounds checking is defensively added. Unit and integration tests validate all routing branches.

✓ Security Reliability

This PR adds AddSwitch support to the durable workflow runner with well-structured routing logic. The code follows established patterns (catch-and-log-and-drop for routing failures, consistent with DurableDirectEdgeRouter), includes proper bounds checking on assigner-returned indices, and uses safe System.Text.Json deserialization with types known at startup. No security vulnerabilities, resource leaks, or unsafe deserialization patterns were found.

✓ Test Coverage

The PR adds good unit tests covering the core switch routing scenarios (matching case, default fallback, plain fan-out) plus an integration test that exercises all three branches end-to-end. The tests use meaningful assertions that verify specific routing outcomes rather than just 'no exception'. However, the error-handling path in DurableFanOutEdgeRouter (where the assigner throws an exception and the message is silently dropped) has no test coverage, which is notable given it represents a message-loss scenario.

✓ Failure Modes

The PR correctly implements switch routing support for the durable workflow runner, faithfully mirroring the in-process FanOutEdgeRunner's target selection logic. The code is well-structured with proper deduplication of deserialization logic. One operational concern: when the edge assigner throws (e.g., due to deserialization failure or a user predicate exception), the exception is caught and swallowed with only a Warning log, causing the orchestration to hang indefinitely. The in-process runtime re-throws in this scenario, giving the caller a clear failure signal. Since this is the sole routing path for the source executor, there is no fallback.

✓ Design Approach

I found one blocking design regression. The durable routing change models selector-based fan-out at the source-executor level instead of the edge level, which causes durable workflows to silently drop other outgoing edges from the same source (for example, a switch plus an additional direct edge, or multiple selector fan-outs).

Suggestions

  • Consider whether the catch block in DurableFanOutEdgeRouter.RouteMessage (line 101-104) should re-throw (or mark the orchestration as failed) rather than swallowing the exception, since unlike DurableDirectEdgeRouter where other edges can still fire, this is the only routing path and swallowing causes a silent workflow hang. The in-process FanOutEdgeRunner re-throws in this scenario (FanOutEdgeRunner.cs:58-61).

Automated review by kshyju's agents

Comment thread dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowAnalyzer.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.

Add the UTF-8 BOM required by dotnet/.editorconfig (charset = utf-8-bom)
to Executors.cs and Program.cs, which was lost during EOL normalization
and caused the dotnet format CHARSET check to fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the UTF-8 BOM required by dotnet/.editorconfig (charset = utf-8-bom)
to the new test file, which caused the dotnet format CHARSET check to fail.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kshyju kshyju marked this pull request as ready for review June 25, 2026 21:06
@kshyju kshyju requested review from a team, ahmedmuhsin and cgillum June 25, 2026 21:06

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 5 | Confidence: 90%

✓ Correctness

The PR correctly implements AddSwitch support in the durable workflow runner. The routing logic in DurableEdgeMap properly handles multiple switches from the same source, sibling direct edges, and shared targets through a well-designed selector-sink counting mechanism. DurableFanOutEdgeRouter correctly evaluates the assigner with per-index bounds checking, mirroring the in-process FanOutEdgeRunner's behavior. The DurableSerialization.DeserializeMessage refactoring is a clean deduplication. All previously-resolved review comments have been addressed. No correctness bugs found.

✓ Security Reliability

This PR is well-structured from a security and reliability perspective. Exception handling around deserialization and selector evaluation is appropriate - failures are caught, logged, and don't crash the orchestration. Per-index range checking ensures a single bad index doesn't drop valid deliveries. The selectorSinkCounts logic correctly handles sibling edges coexisting with selector targets. No injection risks, resource leaks, or unguarded failure modes were identified.

✓ Test Coverage

The test coverage for the new AddSwitch/fan-out selector feature is strong overall, covering the core routing logic, edge cases like duplicate/out-of-range indices, sibling edges, and multiple switches. However, there are two notable gaps: (1) no test verifies the behavior when the edge assigner throws an exception (the catch block in DurableFanOutEdgeRouter lines 96-100), and (2) no test exercises a conditional sibling edge alongside a switch (the EdgeConditions lookup at DurableEdgeMap line 143). Both are new code paths that could silently fail without test coverage.

✓ Failure Modes

The PR introduces switch/selector support for durable fan-out routing. The implementation follows established patterns (log-and-drop on evaluation failure) consistent with the existing DurableDirectEdgeRouter. The selector evaluation, per-index range checking, sibling-edge wiring, and multiple-switch support are well-structured and thoroughly tested. No new silent failure modes beyond the pre-existing design pattern are introduced.

✗ Design Approach

The routing changes mostly line up with the PR goal, but there is one design-level correctness gap: once this PR allows the same target to be reached twice from the same source (for example, a selected switch case plus a sibling direct edge), the durable runner still interprets that as fan-in and aggregates those two deliveries into a single executor invocation. That does not match the in-process contract, where aggregation is reserved for explicit fan-in edges.

Flagged Issues

  • When a selector switch case and a sibling direct edge both route to the same target, WorkflowAnalyzer.AddSuccessorsFromEdge records duplicate predecessor entries (WorkflowAnalyzer.cs:144-152), causing IsFanInExecutor to return true (DurableEdgeMap.cs:235-237) and CollectExecutorInputs to aggregate the two messages into one array (DurableWorkflowRunner.cs:319-323). In-process execution only aggregates behind explicit FanInEdgeData (Execution/EdgeMap.cs:40-44; WorkflowBuilder.cs:503-505), so this changes execution semantics—the target runs once with aggregated input instead of twice independently.

Suggestions

  • Add a unit test verifying that when the edge assigner throws an exception, no message is delivered to any target and no exception escapes (exercising the catch block at DurableFanOutEdgeRouter.cs:96-100). A test like RouteMessage_SelectorThrows_DoesNotDeliverOrThrow with an assigner (_, _) => throw new InvalidOperationException("boom") would cover this.

Automated review by kshyju's agents

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

When a selector switch case and a sibling direct edge both route to the same target, WorkflowAnalyzer.AddSuccessorsFromEdge records duplicate predecessor entries (WorkflowAnalyzer.cs:144-152), causing IsFanInExecutor to return true (DurableEdgeMap.cs:235-237) and CollectExecutorInputs to aggregate the two messages into one array (DurableWorkflowRunner.cs:319-323). In-process execution only aggregates behind explicit FanInEdgeData (Execution/EdgeMap.cs:40-44; WorkflowBuilder.cs:503-505), so this changes execution semantics—the target runs once with aggregated input instead of twice independently.


Source: automated DevFlow PR review

A single source can reach the same target through more than one edge (for
example a switch case plus a sibling direct edge to the same executor). Counting
those repeated deliveries as multiple predecessors made the target look like a
fan-in point, so the durable runner aggregated the deliveries into one
invocation instead of running the target once per delivery as in-process does.

Count distinct predecessor sources so only genuine fan-in (multiple distinct
sources) is aggregated. Adds regression tests for the selector-throws path, a
conditional sibling edge alongside a switch, and fan-in detection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kshyju

kshyju commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Addressed in efe227c:

Design Approach (fan-in mis-aggregation): Confirmed and fixed. AddSuccessorsFromEdge records one predecessor entry per edge occurrence, so a switch case plus a sibling direct edge to the same target from the same source produced Predecessors[T] = [S, S] -> count > 1 -> IsFanInExecutor true -> the runner aggregated both deliveries into a single invocation. Fan-in detection now counts distinct predecessor sources (DurableEdgeMap constructor), so a target fed twice by one source runs once per delivery (matching the in-process FanInEdgeData contract), while genuine fan-in from multiple distinct sources is unchanged. graphInfo.Predecessors is consumed only for this count, so the change is localized.

Test coverage: Added the suggested RouteMessage_SelectorThrows_DoesNotDeliverOrThrow (exercises the selector catch path), plus RouteMessage_SwitchWithConditionalSiblingEdge_HonorsSiblingCondition (covers the EdgeConditions lookup for a conditional sibling edge), and two fan-in detection tests (distinct-source fan-in stays true; same-source switch+sibling is not fan-in).

Full unit suite green (164/164).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Bug]: Workflow switches and conditions does not work as expected when used as Durable workflows

3 participants