项目
版本

如何处理每个请求的生命周期?

对于具有请求/响应语义的应用(如 ASP.NET MVCWeb API),你可以注册依赖项为 “每个请求一个实例” ,这意味着在应用程序处理的每个请求中,都会得到给定依赖项的一个实例,并且该实例与单个请求的生命周期相关联。

要理解每个请求的生命周期,首先需要了解 依赖项生命周期范围的一般工作原理 。一旦你明白了依赖项生命周期范围的工作方式,处理每个请求的生命周期就很简单了。

关于 ASP.NET Core

ASP.NET Core 集成文档 所述,**ASP.NET Core 没有特定的每个请求的生命周期。**所有内容都注册为 InstancePerLifetimeScope(),而不是 InstancePerRequest()

注册按请求的依赖项

当你想要注册为按请求的依赖项时,使用 InstancePerRequest() 注册扩展方法:

var builder = new ContainerBuilder();
builder.RegisterType<ConsoleLogger>()
       .As<ILogger>()
       .InstancePerRequest();
var container = builder.Build();

每当你的应用程序收到一个入站请求时,你都会得到组件的新实例。处理请求级生命周期范围的创建和清理通常通过应用类型的 Autofac 应用集成库 来完成。

如何处理每个请求的生命周期

每个请求的生命周期利用了 标记生命周期范围和“根据匹配生命周期范围的实例”机制 。Autofac 应用集成库会针对不同的应用类型进行挂钩,在入站请求时,它们会创建一个带有标识其为请求生命周期范围的嵌套生命周期范围:

    +--------------------------+
    |    Autofac Container     |
    |                          |
    | +----------------------+ |
    | | Tagged Request Scope | |
    | +----------------------+ |
    +--------------------------+

当你将组件注册为 InstancePerRequest() 时,你告诉 Autofac 在标记为请求范围的生命周期范围内查找并从那里解析组件。这样,如果在单个请求期间存在单元工作生命周期范围,那么按请求的依赖项将在请求期间共享:

    +----------------------------------------------------+
    |                 Autofac Container                  |
    |                                                    |
    | +------------------------------------------------+ |
    | |              Tagged Request Scope              | |
    | |                                                | |
    | | +--------------------+  +--------------------+ | |
    | | | Unit of Work Scope |  | Unit of Work Scope | | |
    | | +--------------------+  +--------------------+ | |
    | +------------------------------------------------+ |
    +----------------------------------------------------+

请求范围被标记为常量值 Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag,它等于字符串 AutofacWebRequest。如果找不到请求生命周期范围,你会得到一个 DependencyResolutionException,告诉你找不到请求生命周期范围。

有关此异常的调试提示,请参阅下面的 “调试” 部分。

不需要请求的跨应用共享依赖项

你可能会遇到一个常见情况,即有一个单独的 Autofac 模块 ,它执行一些依赖项注册,并希望在两个应用程序之间共享这个模块——一个是支持按请求的(如 Web API 应用),另一个不支持(如控制台应用程序或 Windows 服务)。

如何在支持按请求注册和允许注册共享时注册依赖项?

这个问题有几个潜在的解决方案。

选项 1:InstancePerRequest() 注册更改为 InstancePerLifetimeScope()。大多数应用程序不会在任何地方创建自己的子生命周期范围;相反,唯一真正创建的子生命周期范围是请求生命周期。如果你的应用程序就是这样,那么 InstancePerRequest()InstancePerLifetimeScope() 实际上会变得等效。你会得到相同的行为。在不支持按请求语义的应用程序中,你可以根据需要为组件共享创建子生命周期范围。

var builder = new ContainerBuilder();

// 如果您的应用程序在任何地方都没有创建自己的子生命周期范围,
// 则将此...
//
// builder.RegisterType<ConsoleLogger>()
//        .As<ILogger>()
//        .InstancePerRequest();
//
// 更改为:
builder.RegisterType<ConsoleLogger>()
       .As<ILogger>()
       .InstancePerLifetimeScope();
var container = builder.Build();

选项 2: 设置注册模块,以便根据参数指示使用哪种生命周期范围注册类型。

public class LoggerModule : Module
{
  private bool _perRequest;
  public LoggerModule(bool supportPerRequest)
  {
    this._perRequest = supportPerRequest;
  }

  protected override void Load(ContainerBuilder builder)
  {
    var reg = builder.RegisterType<ConsoleLogger>().As<ILogger>();
    if(this._perRequest)
    {
      reg.InstancePerRequest();
    }
    else
    {
      reg.InstancePerLifetimeScope();
    }
  }
}

// 在每个应用程序中注册模块,并传递适当的参数,表示应用程序是否支持按请求注册,例如:
// builder.RegisterModule(new LoggerModule(true));

选项 3: 更复杂但更复杂的第三个选项是在自然不支持这些语义的应用程序中实现自定义的按请求语义。例如,Windows 服务不一定有按请求的语义,但如果它自己托管了一个接受请求并提供响应的自定义服务,你可以为每个请求添加按请求的生命周期范围,并启用对按请求依赖项的支持。更多关于这一点的信息,请参阅 “自定义语义” 部分。

测试带有按请求依赖项的应用

如果你的应用程序注册了按请求的依赖项,你可能希望在单元测试中重用注册逻辑来设置依赖项。当然,你会发现你的单元测试没有请求生命周期范围,因此你会遇到一个 DependencyResolutionException,表明找不到 AutofacWebRequest 范围。如何在测试环境中使用这些注册?

选项 1: 为每个特定测试套件创建自定义注册。特别是在单元测试环境中,你可能不应该为测试设置整个真实的运行时环境——你应该为外部所需的依赖项提供测试双。考虑模拟依赖项,而不是在单元测试环境中执行完整的共享注册。

选项 2: 查看“共享依赖项”部分中的注册共享选择。你的单元测试可以被视为“不支持按请求注册的应用程序”,因此使用允许在不同应用程序类型之间共享的机制可能是合适的。

选项 3: 实现一个测试用的“请求”。这里的意图是,在测试运行之前创建一个真正的 Autofac 生命周期范围,并带有 AutofacWebRequest 标签,运行测试,然后释放假的“请求”范围——就像实际运行了一个完整请求一样。这有点复杂,而且方法取决于应用程序类型。

模拟 MVC 请求范围

[Autofac ASP.NET MVC 集成](../Integration/Mvc.md 使用 ILifetimeScopeProvider 实现以及 AutofacDependencyResolver 动态创建按需请求范围。要模拟 MVC 请求范围,你需要提供一个测试 ILifetimeScopeProvider,它不涉及实际的 HTTP 请求。一个简单的版本可能如下所示:

public class SimpleLifetimeScopeProvider : ILifetimeScopeProvider
{
  private readonly IContainer _container;
  private ILifetimeScope _scope;

  public SimpleLifetimeScopeProvider(IContainer container)
  {
    this._container = container;
  }

  public ILifetimeScope ApplicationContainer
  {
    get { return this._container; }
  }

  public void EndLifetimeScope()
  {
    if (this._scope != null)
    {
      this._scope.Dispose();
      this._scope = null;
    }
  }

  public ILifetimeScope GetLifetimeScope(Action<ContainerBuilder> configurationAction)
  {
    if (this._scope == null)
    {
      this._scope = (configurationAction == null)
             ? this.ApplicationContainer.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag)
             : this.ApplicationContainer.BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag, configurationAction);
    }

    return this._scope;
  }
}

当你从构建的应用容器创建 AutofacDependencyResolver 时,你需要手动指定简单的生命周期范围提供程序。确保在测试运行之前设置了解析器,然后在测试运行后清理假请求范围。在 NUnit 中,它看起来像这样:

private IDependencyResolver _originalResolver = null;
private ILifetimeScopeProvider _scopeProvider = null;

[TestFixtureSetUp]
public void TestFixtureSetUp()
{
  // 构建容器,然后...
  this._scopeProvider = new SimpleLifetimeScopeProvider(container);
  var resolver = new AutofacDependencyResolver(container, provider);
  this._originalResolver = DependencyResolver.Current;
  DependencyResolver.SetResolver(resolver);
}

[TearDown]
public void TearDown()
{
  // 清理假的 '请求' 范围。
  this._scopeProvider.EndLifetimeScope();
}

[TestFixtureTearDown]
public void TestFixtureTearDown()
{
  // 如果你正在摆弄静态,总是把东西恢复原样!
  DependencyResolver.SetResolver(this._originalResolver);
}

模拟 Web API 请求范围

在 Web API 中,请求的生命周期实际上作为 HttpRequestMessage 对象的一部分,作为 ILifetimeScope 对象在整个系统中移动。要模拟一个请求生命周期,你只需要从正在处理的测试消息中获取 ILifetimeScope

在测试设置期间,你应该像在应用程序中那样构建依赖注入解析器,并将其与 HttpConfiguration 对象关联起来。在每个测试中,根据正在测试的用例创建适当的 HttpRequestMessage,然后使用内置的 Web API 扩展方法将配置附加到消息并从消息中获取请求生命周期。

在 NUnit 中,它可能看起来像这样:

private HttpConfiguration _configuration = null;

[TestFixtureSetUp]
public void TestFixtureSetUp()
{
    // 构建容器,然后...
    this._configuration = new HttpConfiguration
    {
        DependencyResolver = new AutofacWebApiDependencyResolver(container)
    }
}

[TestFixtureTearDown]
public void TestFixtureTearDown()
{
    // 清理 - 自动处理清理依赖解析器。
    this._configuration.Dispose();
}

[Test]
public void MyTest()
{
    // 释放 HttpRequestMessage 以释放请求生命周期。
    using (var message = CreateTestHttpRequestMessage())
    {
        message.SetConfiguration(this._configuration);

        // 现在进行你的测试。使用扩展方法
        // message.GetDependencyScope()
        // 从 Web API 获取请求生命周期。
    }
}

解决每个请求依赖问题的技巧

在处理每个请求依赖时,有一些需要注意的地方。这里有一些故障排查帮助。

没有匹配 'AutofacWebRequest' 标签的范围

当人们开始使用每个请求的生命周期时,一个非常常见的异常是:

没有与 'AutofacWebRequest' 标签匹配的范围可见于实例请求的范围。
如果在执行 Web 应用程序时看到此异常,通常表示注册为 HTTP 请求的组件正在被 `SingleInstance()` 组件(或其他类似场景)请求。在 Web 集成中,始终从依赖解析器或请求生命周期范围内请求依赖,而不是直接从容器本身请求。

这意味着应用程序试图解决一个已注册为 InstancePerRequest() 的依赖项,但没有任何请求生命周期。

常见原因包括:

  • 应用程序注册被跨应用程序类型共享。
  • 单元测试运行时使用实际应用程序注册,但没有模拟每个请求的生命周期。
  • 有一个组件 存活时间超过一个请求,但它依赖一个 只存活一个请求 的组件。例如,单例组件依赖一个按请求注册的服务。
  • 在 ASP.NET 应用程序启动时(如 Global.asax 中)运行的代码,或者在没有活跃请求的情况下使用依赖解析。
  • 在没有请求语义的“后台线程”(如 ASP.NET MVC 的 DependencyResolver 进行服务定位)上运行的代码。

追踪问题的根源可能很困难。在很多情况下,你可能会查看正在解决的内容,并看到被解决的组件 不是按请求注册的,而且该组件使用的依赖项 也不是按请求注册的。在这种情况下,你可能需要沿着依赖链一路追踪。异常可能是来自依赖链深处的某个地方。通常,仔细检查堆栈跟踪可以帮助你。如果你正在使用 动态程序集扫描 来定位 模块 进行注册,那么问题注册的来源可能不会立即明显。

在分析问题依赖链中的注册时,查看它们注册的生命周期范围。如果你有一个注册为 SingleInstance() 的组件,但它(可能是间接地)消费了一个注册为 InstancePerRequest() 的组件,那就是一个问题。SingleInstance() 组件会在第一次解决时获取其依赖项,并且永远不会释放。如果这发生在应用程序启动时或在没有当前请求的后台线程上,你会看到这个异常。你可能需要调整一些组件的生命周期范围。再次强调,了解 依赖生命周期范围的一般工作原理 是非常有用的。

无论如何,在某个点上,某些东西正在寻找一个请求生命周期范围,但找不到。

如果你试图跨应用程序类型共享注册,请参阅 sharing-dependencies 部分。

如果你试图使用每个请求依赖进行单元测试,testingsharing-dependencies 部分可以给你一些提示。

如果你的 ASP.NET MVC 应用程序中存在应用程序启动代码或后台线程尝试使用 DependencyResolver.Current - AutofacDependencyResolver 需要在 Web 上下文中进行依赖解决。当你尝试从解析器中解决问题时,它会尝试启动一个按请求生命周期范围并将其存储在当前 HttpContext 旁边。如果没有当前上下文,事情就会失败。访问 AutofacDependencyResolver.Current 并不能绕过这个问题 - 当前解析器属性的工作方式是,它从当前 Web 请求范围中查找自身(这样做是为了允许与 Glimpse 等其他仪器机制一起工作)。

对于应用程序启动代码或后台线程,你可能需要考虑使用不同的服务定位机制,如 公共服务定位器 来避免按请求范围的需求。如果你这样做,你也需要查看 sharing-dependencies 部分来更新你的组件注册,以便它们不一定需要按请求范围。

Web API 中的按请求过滤器依赖

如果你使用 Web API 集成AutofacWebApiFilterProvider 将依赖注入到动作过滤器中,你可能会注意到 过滤器中的依赖项只被一次性解决,而不是按请求基础

这是 Web API 的一个局限性。Web API 内部创建过滤器实例并缓存它们,永远不会重新创建。这移除了任何可能存在的按请求基础的“挂钩”,可以在过滤器中做任何事情。

如果你需要在过滤器中按请求做某事,你需要使用服务定位,并手动从上下文在过滤器中获取请求生命周期范围。例如,一个 ActionFilterAttribute 可能看起来像这样:

public class LoggingFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext context)
    {
        var logger = context.Request.GetDependencyScope().GetService(typeof(ILogger)) as ILogger;
        logger.Log("Executing action.");
    }
}

使用这种服务定位机制,你甚至不需要 AutofacWebApiFilterProvider - 即使不使用 Autofac 也可以做到这一点。

实现自定义按请求语义

你可能有一个自定义的应用程序处理请求 - 类似于接收请求、执行一些工作并提供输出的 Windows 服务应用程序。在这种情况下,如果你正确地组织你的应用程序,你可以实现一个自定义机制,提供按请求注册和解决依赖的能力。你采取的步骤与其他支持按请求语义的自然应用程序类型中的步骤完全相同。

  • 在应用程序启动时构建容器。 进行注册,构建容器,并存储对全局容器的引用,供稍后使用。
  • 当接收到逻辑请求时,创建一个请求生命周期范围。 请求生命周期范围应使用标签 Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag 标记,这样你就可以使用标准注册扩展方法,如 InstancePerRequest()。这还将使你能够跨应用程序类型共享注册模块,如果你愿意的话。
  • 将请求生命周期范围与请求关联。 这意味着你需要在请求内部获取请求范围的能力,而不是有一个静态的全局变量保存“请求范围” - 这是一个线程问题。你需要像 ASP.NET 中的 HttpContext.Current 或 WCF 中的 OperationContext.Current 这样的构造,或者你需要将请求生命周期与实际传入请求信息(如 Web API)一起存储。
  • 请求完成后释放请求生命周期。 在处理完请求并发送响应后,需要调用 IDisposable.Dispose() 在请求生命周期范围内,以确保内存清理和服务实例释放。
  • 在应用程序结束时释放容器。 当应用程序关闭时,调用全局应用程序容器的 IDisposable.Dispose() 以确保所有托管资源得到正确释放,数据库等连接关闭。

如何实现取决于你的应用程序,因此无法提供一个“示例”。查看不同应用类型的集成库(如 MVC 和 Web API)的源代码来了解这些是如何做的,这是一个好方法。然后你可以采用这些模式并相应地进行调整,以适应你的应用程序需求。

这是一个非常高级的过程。 如果你不适当地释放东西,可能会引入内存泄漏;如果错误地将请求生命周期与请求关联起来,可能会导致线程问题。如果你走这条路,一定要做大量测试和性能分析,确保一切按预期工作。

在本文档中