Description
DurableWorkflowOptions.AddWorkflow writes into its backing dictionary with an indexer assignment, so registering a second workflow under a name that is already taken silently replaces the first one:
// dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs
this._workflows[workflow.Name] = workflow;
Workflow names are expected to be unique — they are the identity used to derive the orchestration function name (dafx-{workflowName}). But nothing enforces that, and a collision produces no exception, no log, and no startup failure.
This is inconsistent with agent registration, which already treats a duplicate name as an error:
// DurableAgentsOptions.AddAIAgent
if (this._agentFactories.ContainsKey(agent.Name))
{
throw new ArgumentException($"An agent with name '{agent.Name}' has already been registered.", nameof(agent));
}
Impact
A collision leaves the registration state internally inconsistent rather than simply dropping one workflow.
Given workflows A and B both named OrderPipeline, registered in that order:
Workflows["OrderPipeline"] resolves to B, so B's graph is what the orchestration executes.
- The executor registry already took A's bindings for every executor name the two share, because
ExecutorRegistry.Register uses TryAdd (first writer wins) and entries are never evicted.
- Activity/orchestration registration walks only the surviving entry:
// ServiceCollectionExtensions.RegisterTasksFromOptions
foreach (Workflow workflow in workflowOptions.Workflows.Values.ToList())
The result is that the dafx-OrderPipeline orchestration runs B's topology while executing A's code for any same-named executor. Workflow A is otherwise absent from the application with no indication that it was ever registered.
Repro
Workflow a = new WorkflowBuilder(new FunctionExecutor<string>("Step", HandlerA))
.WithName("OrderPipeline").Build();
Workflow b = new WorkflowBuilder(new FunctionExecutor<string>("Step", HandlerB))
.WithName("OrderPipeline").Build();
options.AddWorkflow(a);
options.AddWorkflow(b);
// options.Workflows.Count == 1 (a was silently discarded)
// options.Workflows["OrderPipeline"] is b (last writer won)
// Executors["Step"].Binding is a's (first writer won)
Verified against the current main behavior with a local test.
Accidental path into it
Sub-workflows are registered into the same flat dictionary, so a sub-workflow sharing a name with a top-level workflow silently replaces it:
// ServiceCollectionExtensions.BuildWorkflowRegistrationRecursive
Workflow subWorkflow = subworkflowBinding.WorkflowInstance;
workflowOptions.AddWorkflow(subWorkflow);
Implementation note
AddWorkflow cannot simply switch to Dictionary.Add and throw unconditionally. The sub-workflow path above re-adds a sub-workflow on every parent traversal, so registering a parent and its sub-workflow explicitly re-adds that sub-workflow with the same instance. RegisterTasksFromOptions can also run more than once against the shared DurableOptions. Both are legitimate and must keep working.
Note that the same Workflow instance cannot be shared as a sub-workflow of two parents: Workflow.TakeOwnership in Microsoft.Agents.AI.Workflows claims single ownership at BindAsExecutor time and throws Cannot use a Workflow as a subworkflow of multiple parent workflows. Sharing therefore requires two separately built instances, which would carry the same Name and be rejected by the check proposed here. That scenario is out of scope for this issue and can be revisited separately.
The check needs to reject only a name that maps to a different workflow instance, and remain idempotent when the same instance is re-registered.
Acceptance criteria
- Registering two different workflow instances under the same name throws, with a message consistent with the existing
AddAIAgent duplicate-name error.
- Re-registering the same workflow instance under the same name remains a no-op and does not throw.
- Sub-workflow registration continues to work.
- Test coverage for all three cases.
Notes
Pre-existing since the initial extension migration (#12); the indexer assignment has been in place since that commit. Unrelated to #39 — the bulk AddWorkflows method removed there was a plain loop over AddWorkflow with no duplicate detection of its own, so collision behavior is identical before and after that change.
Description
DurableWorkflowOptions.AddWorkflowwrites into its backing dictionary with an indexer assignment, so registering a second workflow under a name that is already taken silently replaces the first one:Workflow names are expected to be unique — they are the identity used to derive the orchestration function name (
dafx-{workflowName}). But nothing enforces that, and a collision produces no exception, no log, and no startup failure.This is inconsistent with agent registration, which already treats a duplicate name as an error:
Impact
A collision leaves the registration state internally inconsistent rather than simply dropping one workflow.
Given workflows
AandBboth namedOrderPipeline, registered in that order:Workflows["OrderPipeline"]resolves to B, so B's graph is what the orchestration executes.ExecutorRegistry.RegisterusesTryAdd(first writer wins) and entries are never evicted.The result is that the
dafx-OrderPipelineorchestration runs B's topology while executing A's code for any same-named executor. Workflow A is otherwise absent from the application with no indication that it was ever registered.Repro
Verified against the current
mainbehavior with a local test.Accidental path into it
Sub-workflows are registered into the same flat dictionary, so a sub-workflow sharing a name with a top-level workflow silently replaces it:
Implementation note
AddWorkflowcannot simply switch toDictionary.Addand throw unconditionally. The sub-workflow path above re-adds a sub-workflow on every parent traversal, so registering a parent and its sub-workflow explicitly re-adds that sub-workflow with the same instance.RegisterTasksFromOptionscan also run more than once against the sharedDurableOptions. Both are legitimate and must keep working.Note that the same
Workflowinstance cannot be shared as a sub-workflow of two parents:Workflow.TakeOwnershipinMicrosoft.Agents.AI.Workflowsclaims single ownership atBindAsExecutortime and throwsCannot use a Workflow as a subworkflow of multiple parent workflows.Sharing therefore requires two separately built instances, which would carry the sameNameand be rejected by the check proposed here. That scenario is out of scope for this issue and can be revisited separately.The check needs to reject only a name that maps to a different workflow instance, and remain idempotent when the same instance is re-registered.
Acceptance criteria
AddAIAgentduplicate-name error.Notes
Pre-existing since the initial extension migration (#12); the indexer assignment has been in place since that commit. Unrelated to #39 — the bulk
AddWorkflowsmethod removed there was a plain loop overAddWorkflowwith no duplicate detection of its own, so collision behavior is identical before and after that change.