microsoft/aspire · error · InvalidOperationException

Circular dependency detected

Error message

Circular dependency detected: {string.Join(" -> ", visited)} -> {parent}

What it means

RelationshipEvaluator.ValidateRelationships walks the child→parent annotation graph (from WaitAnnotation/parent-child annotations) pushing each node onto a visited stack; if a node is re-encountered on the current path, the parent links form a cycle and it throws InvalidOperationException showing the chain. Cycles make a well-defined parent-child (and thus start/stop) ordering impossible, so the AppHost refuses to build the relationship model.

Solutions

  1. Read the cycle chain in the message and remove one WaitFor/wait dependency to break the loop.
  2. Replace the cyclic wait with a one-directional dependency (the genuinely dependent side waits).
  3. If mutual readiness is needed, use health-check/endpoint-based readiness instead of a bidirectional WaitFor.
  4. Search the AppHost source for .WaitFor( calls involving the named resources and reorder.

Example fix

// before (circular)
var a = builder.AddProject<Projects.A>("a").WaitFor(b);
var b = builder.AddProject<Projects.B>("b").WaitFor(a);

// after (acyclic)
var a = builder.AddProject<Projects.A>("a");
var b = builder.AddProject<Projects.B>("b").WaitFor(a);
Defensive patterns

Strategy: validation

Validate before calling

// Detect cycles in wait/parent annotations before building the AppHost model.
var waits = new Dictionary<string, string>(); // child -> parent edges, fill from WaitFor calls
var visiting = new HashSet<string>();
bool HasCycle(string node, HashSet<string> stack)
{
    if (!stack.Add(node)) return true;
    if (waits.TryGetValue(node, out var parent) && HasCycle(parent, stack)) return true;
    stack.Remove(node);
    return false;
}

Try / catch

try
{
    builder.Build(); // or Distributor.Run()
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Circular dependency detected:"))
{
    logger.LogError(ex, "Remove one WaitFor to break the cycle shown in the chain.");
    throw;
}

Prevention

When it happens

Trigger: Adding WaitAnnotation / WaitFor or parent-child relationships between resources such that A waits on B, B waits on C, and C waits on A (directly or transitively) — detected while GetParentChildRelationshipsFromAnnotations builds the graph at app model finalization.

Common situations: Mutual WaitFor calls between two services; adding waits after refactoring without noticing an existing reverse dependency; generating relationships programmatically in loops where each resource waits on the next.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

Thrown at src/Aspire.Hosting/Orchestrator/RelationshipEvaluator.cs:77

        }

        var childToParentLookup = relationships.ToDictionary(x => x.Child, x => x.Parent);

        // ensure no circular dependencies
        var visited = new Stack<IResource>();
        foreach (var relation in relationships)
        {
            ValidateNoCircularDependencies(childToParentLookup, relation.Child, visited);
        }

        static void ValidateNoCircularDependencies(Dictionary<IResource, IResource> childToParentLookup, IResource child, Stack<IResource> visited)
        {
            visited.Push(child);
            if (childToParentLookup.TryGetValue(child, out var parent))
            {
                if (visited.Contains(parent))
                {
                    throw new InvalidOperationException($"Circular dependency detected: {string.Join(" -> ", visited)} -> {parent}");
                }
                ValidateNoCircularDependencies(childToParentLookup, parent, visited);
            }
            visited.Pop();
        }
    }
}

View on GitHub (pinned to 25830f84bd)