项目
版本

Autofac 被囚禁的依赖

当一个设计用于短期存在时间的组件被一个长期存在的组件所持有时,就会发生 “被囚禁的依赖” 现象。Mark Seemann 在这篇文章中很好地解释了这个概念。

Autofac 并不会阻止你创建被囚禁的依赖。 你可能会因为被囚禁的依赖的设置方式而得到解决异常,但并不总是这样。停止产生被囚禁的依赖是开发者的责任。

一般规则

避免被囚禁依赖的一般规则:

消费组件的生命周期应该小于或等于它所消费的服务的生命周期。

简单来说,不要让单例依赖一个“按请求实例”的服务,因为它会被持有太长时间。

简单示例

假设你有一个 Web 应用程序,它使用传入请求的信息来决定应该连接到哪个数据库。你可能有以下组件:

  • 一个接收当前请求和数据库连接工厂的 存储库
  • 类似于 HttpContext当前请求 ,可以用来帮助确定业务逻辑。
  • 接收某种参数并返回正确数据库连接的 数据库连接工厂

在这个例子中,考虑你想为每个组件使用的 生命周期范围当前请求上下文 是一个明显的例子——你想要 “按请求实例”。其他组件呢?

对于 存储库 ,假设你选择 “单例”。单例只会在应用生命周期中创建一次并缓存。如果你选择 “单例”,请求上下文会被传递进来,并且在应用整个生命周期中都会被持有——即使那个当前请求已经结束,过时的请求上下文也会被持有。存储库 生命周期较长,但它持有一个寿命较短的组件。这就是一个被囚禁的依赖。

但是,如果你将 存储库 改为 “按请求实例”,现在它的生存期与当前请求相同,不再更长。这正好与它所需的请求上下文一样长,所以现在它不是被囚禁的。存储库 和请求上下文将在同一时间释放(在请求结束时),一切都会好起来。

再进一步,假设你将 存储库 改为 “按依赖实例” 以每次获取一个新的。这仍然是可以的,因为它打算生存的时间比当前请求要短。它不会长时间持有请求,因此没有被囚禁。

数据库连接工厂也会进行类似的思考过程,但可能需要考虑不同的因素。也许工厂的初始化成本较高,或者需要维护一些内部状态才能正常工作。你可能不希望它是 “按请求实例” 或 “按依赖实例”。实际上,你可能确实需要它作为一个单例。

短生命周期的依赖可以持有长生命周期的依赖。 如果你的 存储库 是 “按请求实例” 或 “按依赖实例”,你仍然会没事。数据库连接工厂故意活得更长。

代码示例

下面的单元测试展示了如何强制创建一个被囚禁的依赖。在这个例子中,使用了一个 “规则管理器” 来处理一组 “规则”,这些规则在整个应用程序中被使用。

public class RuleManager
{
    public RuleManager(IEnumerable<IRule> rules)
    {
        this.Rules = rules;
    }

    public IEnumerable<IRule> Rules { get; private set; }
}

public interface IRule { }

public class SingletonRule : IRule { }

public class InstancePerDependencyRule : IRule { }

[Fact]
public void CaptiveDependency()
{
    var builder = new ContainerBuilder();

    // 规则管理器是一个单例组件。它只会被实例化一次,之后缓存起来。它总是从根生命周期范围(容器)中被解析,因为它需要共享。
    builder.RegisterType<RuleManager>()
           .SingleInstance();

    // 这个规则注册为按依赖实例。每次请求时都会创建一个新的实例。
    builder.RegisterType<InstancePerDependencyRule>()
           .As<IRule>();

    // 这个规则作为单例注册。像规则管理器一样,它只会被解析一次,且来自根生命周期范围。
    builder.RegisterType<SingletonRule>()
           .As<IRule>()
           .SingleInstance();

    using (var container = builder.Build())
    using (var scope = container.BeginLifetimeScope("request"))
    {
        // 规则管理器将是单例。它包含对单例 SingletonRule 的引用,这是没问题的。但是,
        // 它也会持有一个 InstancePerDependencyRule,这可能不是很好。它持有
        // 的 InstancePerDependencyRule 将在 RuleManager 内部的容器生命周期中持续
        // 存在,直到容器被释放。
        var manager = scope.Resolve<RuleManager>();
    }
}

请注意,上面的例子并没有直接显示,但如果你在 container.BeginLifetimeScope() 调用中动态添加规则注册,那么这些动态注册将不会 包含在解析出的 RuleManager 中。规则管理器作为单例,从包含动态注册的根容器中解析。

另一个代码示例显示了如何在创建一个错误地绑定到子生命周期范围的被囚禁依赖时,可能会得到一个异常。

public class RuleManager
{
    public RuleManager(IEnumerable<IRule> rules)
    {
        this.Rules = rules;
    }

    public IEnumerable<IRule> Rules { get; private set; }
}

public interface IRule { }

public class SingletonRule : IRule
{
    public SingletonRule(InstancePerRequestDependency dep) { }
}

public class InstancePerRequestDependency : IRule { }

[Fact]
public void CaptiveDependency()
{
    var builder = new ContainerBuilder();

    // 再次,规则管理器是一个单例组件,从根生命周期中解析并缓存。
    builder.RegisterType<RuleManager>()
           .SingleInstance();

    // 这个规则作为单例注册。像规则管理器一样,它只会被解析一次,且来自根生命周期范围。
    builder.RegisterType<SingletonRule>()
           .As<IRule>()
           .SingleInstance();

    // 这个规则按请求注册。它只存在于请求期间。
    builder.RegisterType<InstancePerRequestDependency>()
           .As<IRule>()
           .InstancePerMatchingLifetimeScope("request");

    using (var container = builder.Build())
    using (var scope = container.BeginLifetimeScope("request"))
    {
        // 问题:当规则管理器作为依赖链的一部分解析时,规则构造函数中的 InstancePerRequestDependency 将
        // 无法解析,因为规则来自根生命周期范围,而 InstancePerRequestDependency 并不存在那里。
        Assert.Throws<DependencyResolutionException>(() => scope.Resolve<RuleManager>());
    }
}

规则的例外

考虑到应用程序开发者最终负责确定是否可以接受被囚禁的依赖,开发者可能会确定单例(例如)可以接受一个 “按依赖实例” 的服务。

例如,也许你有一个故意设置为只存活于消费组件生命周期的缓存类。如果消费者是单例,缓存可以存储整个应用生命周期中的数据;如果消费者是“按请求实例”,它只存储单个网络请求的数据。在这种情况下,你可能会有意无意地让一个生命周期较长的组件依赖于一个生命周期较短的组件。

只要应用程序开发者理解以这样的生命周期设置事物的后果,这就可以接受。也就是说,如果你要这样做,请有意识地去做,而不是意外地。

在本文档中