Skip to content

.NET: Deduplicate fan-out targets so each target runs once - #9432

Merged
Vincent Biret (baywet) merged 2 commits into
microsoft:mainfrom
RachelWanggg:fix/dotnet-fanout-duplicate-targets
Oct 16, 2026
Merged

Vincent Biret (baywet) merged 2 commits into
microsoft:mainfrom
RachelWanggg:fix/dotnet-fanout-duplicate-targets

Conversation

@RachelWanggg

Copy link
Copy Markdown
Contributor

Motivation & Context

A fan-out target selector that returns the same index more than once gets that target run once per entry. FanOutEdgeRunner.ChaseEdgeAsync maps the indices to sink IDs and keeps every entry, so the target appears twice in the DeliveryMapping and MapInto enqueues the envelope twice into its queue. A selector returning [0, 0] runs target 0 twice on the same message. Routing by tags hits this whenever two tags map to the same executor. With an agent executor as the target, the message appears twice in the agent's prompt, and the agent runs twice if the TurnToken is routed the same way.

Description & Review Guide

  • What are the major changes? FanOutEdgeRunner.ChaseEdgeAsync drops repeated target IDs with Distinct(StringComparer.Ordinal) once the IDs are resolved, from the selector or from the target list when there is no selector, and before the envelope-target and CanHandle checks. Distinct keeps the first occurrence, so delivery order follows the order in which each target is first named. This is the change Python: Deduplicate fan-out selection results so each target runs once #8739 made to the Python FanOutEdgeRunner, which also covers both paths.
  • What is the impact of these changes? Each selected target receives the message once. An edge without a selector whose target list names an executor twice now delivers to it once too. Target lists and selections without repeats deliver exactly as before. AddSwitch is unaffected, since SwitchBuilder already collects its case indices in a HashSet<int>. No public API change.
  • What do you want reviewers to focus on? Whether repeated targets should be deduplicated, as here and in Python, or rejected. Rejecting them would turn workflows that run today into errors, so this PR deduplicates.

Three tests in EdgeRunnerTests.cs, all failing on main:

  • Test_FanOutEdgeRunner_DeduplicatesAssignedTargetsAsync: a selector returning [1, 0, 1] over ["executor2", "executor3"] delivers to executor3, then executor2, once each. On main the deliveries are ["executor3", "executor2", "executor3"].
  • Test_FanOutEdgeRunner_DeduplicatesRepeatedSinksAsync: no selector, target list ["executor2", "executor2"], one delivery. On main there are two.
  • Test_FanOutEdge_TargetAssignedTwiceRunsOnceAsync: a workflow built with AddFanOutEdge<string> and a selector returning [0, 0] runs the target executor once. On main it runs twice.

Local verification on macOS arm64, .NET SDK 10.0.402, dotnet test --project tests/<project> -f net10.0, branch on top of deecbcb. The main column is this branch with FanOutEdgeRunner.cs reverted to main.

Unit test project (Microsoft.Agents.AI.<name>.UnitTests) main This PR
Workflows 858 passed, 3 failed (the new tests) 861 passed
Workflows.Declarative 1115 passed 1115 passed
Workflows.Declarative.Mcp not run 90 passed
Workflows.Generators not run 36 passed
Hosting not run 181 passed
Hosting.A2A not run 154 passed
Hosting.AGUI.AspNetCore not run 12 passed
Hosting.OpenAI not run 585 passed
Hosting.AzureStorage not run 2 passed, 12 skipped (Azurite not available)
Foundry.Hosting not run 520 passed, 1 skipped (existing Skip)
DevUI not run 21 passed, 2 skipped (existing Skip)
CosmosNoSql not run 25 passed, 54 skipped (Cosmos DB Emulator not available)
  • Mutation checks, EdgeRunnerTests only: removing the Distinct fails all three new tests. Applying it only when there is no selector fails the two selector tests. Applying it only to selector results fails Test_FanOutEdgeRunner_DeduplicatesRepeatedSinksAsync. Rebuilding the list in target-list order, or sorting it, fails Test_FanOutEdgeRunner_DeduplicatesAssignedTargetsAsync.
  • dotnet format <csproj> --verify-no-changes passes for Microsoft.Agents.AI.Workflows and Microsoft.Agents.AI.Workflows.UnitTests.
  • dotnet build src/Microsoft.Agents.AI.Workflows --warnaserror passes for net10.0, net9.0, net8.0, netstandard2.0 and net472 with 0 warnings.
  • Not run: net472 tests (Windows only), integration tests, and unit test projects that do not depend on Microsoft.Agents.AI.Workflows.

Related Issue

Fixes #9431

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

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 links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • 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.

FanOutEdgeRunner mapped every index returned by a fan-out target selector
to a sink ID and delivered once per entry, so a target selected twice ran
twice on the same message. The same happened without a selector when the
target list named an executor twice. Drop repeated IDs once they are
resolved, keeping the order in which each target is first named.

This comment was marked as off-topic.

@agent-framework-automation agent-framework-automation Bot added .NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows labels Oct 15, 2026
@baywet
Vincent Biret (baywet) requested a balanced review from Copilot October 16, 2026 10:50

@baywet Vincent Biret (baywet) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution!

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.

🟢 Approval recommended

The localized fix covers both routing paths with focused regression tests and no identified blocking issues.

0 open findings

🧠 Review effort: Balanced

@baywet

Copy link
Copy Markdown
Member

depends on #9455 for merge

@baywet
Vincent Biret (baywet) added this pull request to the merge queue Oct 16, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 16, 2026
@baywet
Vincent Biret (baywet) added this pull request to the merge queue Oct 16, 2026
Merged via the queue into microsoft:main with commit ee36283 Oct 16, 2026
32 checks passed

This branch was previously deployed

1 inactive deployment
github-app-auth — 0730c232 Deployed Oct 16, 2026 by baywet via add_label #25608
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net workflows Usage: [Issues, PRs], Target: Workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Bug]: Fan-out edge runs a target twice when the target selector returns its index twice

3 participants