NDLRCOR006: Circular dependency detected¶
Cause¶
A service directly or indirectly depends on itself, creating a circular reference chain. This will cause a runtime exception when the dependency injection container attempts to resolve the service.
Rule Description¶
A circular dependency occurs when the dependency graph contains a cycle:
This is impossible to resolve because:
- To create
A, we needB - To create
B, we needC - To create
C, we needA← but we haven't finished creatingAyet!
// ⚠️ NDLRCOR006: Circular dependency: OrderService → InventoryService → OrderService
[Scoped]
public class OrderService(IInventoryService inventory)
{
public void PlaceOrder() => inventory.Reserve();
}
[Scoped]
public class InventoryService(IOrderService orders) // Depends back on OrderService!
{
public void Reserve() => orders.ValidateStock();
}
What happens at runtime:
System.InvalidOperationException: A circular dependency was detected for the service of type 'IOrderService'.
IOrderService -> OrderService -> IInventoryService -> InventoryService -> IOrderService
Or worse, a StackOverflowException if the container doesn't detect the cycle.
Common Circular Dependency Patterns¶
Direct Cycle (A → B → A)¶
[Scoped] public class UserService(IRoleService roles) { }
[Scoped] public class RoleService(IUserService users) { }
Indirect Cycle (A → B → C → A)¶
[Scoped] public class OrderService(IPaymentService payments) { }
[Scoped] public class PaymentService(INotificationService notifications) { }
[Scoped] public class NotificationService(IOrderService orders) { } // Cycle!
Self-Reference (A → A)¶
How to Fix¶
Option 1: Refactor to Remove the Cycle (Recommended)¶
Often circular dependencies indicate a design problem. Consider:
Extract shared logic:
// Before: OrderService ↔ InventoryService
// After: Both depend on shared service
[Scoped] public class StockValidator { } // Shared logic here
[Scoped] public class OrderService(IStockValidator validator) { }
[Scoped] public class InventoryService(IStockValidator validator) { }
Use events/mediator:
// Before: Direct dependency
[Scoped] public class OrderService(IInventoryService inventory) { }
// After: Decoupled via events
[Scoped] public class OrderService(IEventBus events)
{
public void PlaceOrder() => events.Publish(new OrderPlacedEvent());
}
[Scoped] public class InventoryHandler : IEventHandler<OrderPlacedEvent> { }
Option 2: Use Lazy (Deferred Resolution)¶
Break the cycle by deferring one dependency:
[Scoped]
public class OrderService(IInventoryService inventory) { }
[Scoped]
public class InventoryService(Lazy<IOrderService> orders) // Lazy breaks the cycle
{
public void Reserve() => orders.Value.ValidateStock();
}
Note: This only delays the problem—the cycle still exists and may cause issues.
Option 3: Use Factory/Func¶
Similar to Lazy
[Scoped]
public class InventoryService(Func<IOrderService> orderFactory)
{
public void Reserve()
{
var orders = orderFactory();
orders.ValidateStock();
}
}
Option 4: Property Injection (Not Recommended)¶
Some DI containers support property injection to break cycles:
[Scoped]
public class InventoryService
{
[Inject] public IOrderService Orders { get; set; } // Set after construction
}
Warning: Property injection hides dependencies and makes testing harder. Use sparingly.
Prevention Best Practices¶
-
Single Responsibility: Each service should do one thing. Large services often develop circular dependencies.
-
Dependency Direction: Dependencies should flow one way (e.g., Controllers → Services → Repositories).
-
Interface Segregation: Split large interfaces into smaller, focused ones.
-
Layer Architecture: Use clear layers where higher layers depend on lower layers, never the reverse.
Runtime Detection¶
For comprehensive cycle detection including dynamic registrations:
// Detect all cycles at startup
services.Verify(VerificationOptions.Strict);
// Or get detailed chain information
var result = services.VerifyWithDiagnostics();
foreach (var issue in result.Issues.Where(i => i.Type == VerificationIssueType.CircularDependency))
{
Console.WriteLine(issue.DetailedMessage);
}
Detection Limitations¶
This analyzer detects cycles at compile-time by:
- Building a dependency graph from constructor parameters
- Traversing the graph using DFS to find back-edges
Limitations:
- Only analyzes types with Needlr registration attributes
- Cannot detect cycles involving factory-registered services
- Cannot detect cycles created by runtime configuration
Use the runtime Verify() method for complete coverage.
When to Suppress¶
Generally, do not suppress this diagnostic. Circular dependencies will cause runtime failures.
However, if you're using Lazy<T> or Func<T> to intentionally break the cycle:
#pragma warning disable NDLRCOR006
[Scoped] public class ServiceA(Lazy<IServiceB> b) { } // Lazy breaks cycle
[Scoped] public class ServiceB(IServiceA a) { }
#pragma warning restore NDLRCOR006