目录

DI 的构造函数循环依赖问题

DI 的构造函数循环依赖问题

一、什么是构造函数循环依赖?

用最简单的话说:

两个服务互相依赖对方,导致 DI 容器无法创建实例,形成死锁。

例如:

public class A
{
    public A(B b) { }
}

public class B
{
    public B(A a) { }
}

A 的构造函数需要一个 B
B 的构造函数需要一个 A

➡ DI 容器想创建 A,于是要 B
➡ 要创建 B,又需要 A
➡ 无限循环,爆炸 ❌

武侠理解:

  • A 是“峨眉掌门”,练功需要“华山真气(B)”
  • B 是“华山掌门”,练功需要“峨眉心法(A)”

两派互相都依赖对方才能修炼,所以谁都练不了 → 死循环


二、DI 容器为什么会“死”?

注意:构造函数注入是最强依赖

DI 容器在解析构造函数时必须:

  • 立即
  • 完整
  • 无条件

提供所有依赖。

所以:

构造函数里出现循环依赖 = 容器必死

.NET 默认容器也不支持构造函数循环依赖。


三、怎么发现循环依赖?

出现这种报错:

A circular dependency was detected for the service of type ...

如果你看到这个,“恭喜”,你被循环依赖攻击了。


四、如何解决循环依赖(四大武功)

武功 1:使用接口拆分职责(最正统方式)

80% 的循环依赖都是因为类职责过大、边界不清晰

例如:

public class UserService
{
    public UserService(OrderService orderService) { }
}

public class OrderService
{
    public OrderService(UserService userService) { }
}

UserService 负责用户
OrderService 负责订单
但它们又互相依赖。

正确做法:
🔹 拆分成更小的接口(例如 IUserQuery / IOrderQuery)
🔹 单向依赖

武侠解释:

把“掌门”和“长老”的职责分开,他们互不依赖。


武功 2:使用 Lazy(延迟依赖)

让注入不要立即执行,而是等真正使用时再创建。

public class A
{
    private readonly Lazy<B> _b;

    public A(Lazy<B> b)
    {
        _b = b;
    }
}

容器不会马上创建 B,所以可以解除循环。

武侠解释:
“峨眉掌门”修炼需要“华山真气”,
但他不立刻要,只是说:

“等我需要时再叫他来。”

于是不会挂。


武功 3:使用 Func(工厂注入)

这比 Lazy 更灵活。

public class A
{
    private readonly Func<B> _getB;

    public A(Func<B> getB)
    {
        _getB = getB;
    }
}

武侠解释:
峨眉掌门不是要“华山真气的对象 B”,
而是要“一种能够随时召唤华山真气的方法”。


武功 4:使用 Property Injection / Method Injection

(不推荐,但确实能解决)

public class A
{
    public B B { get; set; }
}

或者:

public void SetB(B b)
{
    this._b = b;
}

武侠解释:
“我练功的时候先不用你,等需要辅助时你再来。”

但副作用多,不干净、难维护。


五、为什么 Scoped 服务中注入 Singleton 会出问题?

(这也会被误认为“循环依赖”)

  • Singleton 生命周期最长
  • Scoped 生命周期短

如果让 Singleton 注入 Scoped,会出现逻辑错误:

Singleton 想要依赖一个随着请求变化的对象 → 危险

这不是循环依赖,但会导致:

Cannot consume scoped service from singleton

六、总结

方法 推荐度 适用场景

拆分接口(解耦)⭐⭐⭐⭐⭐ 根本性解决,最优雅

Lazy ⭐⭐⭐⭐ 有少量延迟依赖

Func ⭐⭐⭐⭐ 动态创建

属性注入 ⭐⭐ 无法修改架构时临时方法