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
- Read the cycle chain in the message and remove one WaitFor/wait dependency to break the loop.
- Replace the cyclic wait with a one-directional dependency (the genuinely dependent side waits).
- If mutual readiness is needed, use health-check/endpoint-based readiness instead of a bidirectional WaitFor.
- 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
- Keep WaitFor relationships strictly one-directional between any pair of resources.
- Draw the dependency graph in the AppHost when using more than a few WaitFor calls.
- Avoid programmatic loops that add WaitFor between successive resources.
- Use health checks or resource events instead of WaitFor for mutual readiness needs.
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
- ConnectionStringAvailableEvent published for resource
- Resource ' ' stopped waiting for dependency resource ' '…
- Stopped waiting for resource
- A circular lifetime reference was detected for resource
- A global MCP approval policy cannot be combined with custom…
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)