microsoft/aspire · error · InvalidOperationException

Circular dependency detected in pipeline steps

Error message

Circular dependency detected in pipeline steps: {string.Join(" → ", cycle)}

What it means

Before executing, the pipeline topologically sorts steps (DFS with VisitState tracking). If the DFS re-enters a step that is currently on the visiting stack, the steps form a cycle; the pipeline throws an InvalidOperationException listing the cycle path joined with ' → ', naming each step in the loop.

Solutions

  1. Read the cycle path in the message and break the loop by removing or correcting one DependsOn/RequiredBy edge
  2. Decide the true ordering: keep only the edge in the direction that should execute first
  3. If the ordering genuinely is mutual, merge the two steps into one or split the shared work into a third step both depend on
  4. Draw the graph from all DependsOnSteps/RequiredBySteps registrations before wiring new steps to spot cycles early

Example fix

// before
stepA.DependsOn("B");
stepB.DependsOn("A"); // cycle
// after
stepA.DependsOn("B"); // B runs first, then A
Defensive patterns

Strategy: validation

Validate before calling

// Before wiring, assert the dependency graph is acyclic
var deps = steps.ToDictionary(s => s.Name, s => s.DependsOnSteps);
var visiting = new HashSet<string>(); var done = new HashSet<string>();
void Visit(string n)
{
    if (done.Contains(n)) return;
    if (!visiting.Add(n)) throw new InvalidOperationException($"Cycle at {n}");
    foreach (var d in deps.GetValueOrDefault(n, Enumerable.Empty<string>())) Visit(d);
    visiting.Remove(n); done.Add(n);
}
foreach (var n in deps.Keys) Visit(n);

Try / catch

try { await pipeline.ExecuteAsync(); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Circular dependency detected"))
{
    // ex.Message contains the full cycle path 'A → B → A'; remove one edge
}

Prevention

When it happens

Trigger: Registering steps whose DependsOnSteps/RequiredBySteps edges form a loop, e.g. A depends on B, B depends on A; or A depends on B, B depends on C, C depends on A. Also occurs when RequiredBySteps edges, combined with dependency edges, accidentally create mutual ordering between two steps.

Common situations: Wiring steps symmetrically by mistake (A.DependsOn(B) and B.DependsOn(A)); copy-pasting dependency wiring that points a later step back to an earlier one; a refactor that moved a dependency from one step to another creating an unintended loop; string-based wiring across packages where two integrations each declare they depend on the other's step.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/2cc90ca1209ab8f1. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs:1097

        var visitStates = new Dictionary<string, VisitState>(steps.Count, StringComparer.Ordinal);
        foreach (var step in steps)
        {
            visitStates[step.Name] = VisitState.Unvisited;
        }

        // DFS to detect cycles
        void DetectCycles(string stepName, Stack<string> path)
        {
            if (!visitStates.TryGetValue(stepName, out var state))
            {
                return;
            }

            if (state == VisitState.Visiting) // Currently visiting - cycle detected!
            {
                var cycle = path.Reverse().SkipWhile(s => s != stepName).Append(stepName);
                throw new InvalidOperationException(
                    $"Circular dependency detected in pipeline steps: {string.Join(" → ", cycle)}");
            }

            if (state == VisitState.Visited) // Already fully visited - no need to check again
            {
                return;
            }

            visitStates[stepName] = VisitState.Visiting;
            path.Push(stepName);

            if (stepsByName.TryGetValue(stepName, out var step))
            {
                foreach (var dependency in step.DependsOnSteps)
                {
                    DetectCycles(dependency, path);
                }
            }

View on GitHub (pinned to 25830f84bd)