使用 Microsoft.Extensions.DependencyInjection 创建子容器(或隔离范围)的最佳策略

Joh*_*oop 7 dependency-injection .net-core asp.net-core

在我的 AspNetCore 应用程序中,我处理从队列到达的消息。为了处理消息,我需要解析一些服务。其中一些服务依赖于ITenantId,我使用收到的消息中的信息进行绑定。为了解决这个问题,消息的处理从创建一个子容器开始,然后我用它来创建一个IServiceScope,从中解析所有需要的依赖项。

消息可以并行处理,因此作用域必须相互隔离。

我可以看到创建子容器的方法,但我不确定哪种方法在性能、内存更改等方面最好:

选项 A:每次消息到达时,将 IServiceCollection 克隆到新的 ServiceCollection 中,并ITenantId在克隆的实例上重新绑定。

选项 B:当程序启动时,创建 IServiceCollection 的不可变副本(使用ImmutableList<ServiceDescriptor>ImmutableArray<ServiceDescriptor>)。每次消息到达时,替换ITenantId(产生 的新实例ImmutableList<ServiceDescriptor>)并调用CreateScope()新的不可变实例。

我不喜欢选项 A 的一点是,每次消息到达时都需要克隆整个服务集合。我不确定选项 B 中的不可变集合是否以更智能的方式处理这个问题?

Ste*_*ven 10

这两个选项都会导致为每个传入消息创建一个新的容器实例。尽管这允许每条消息在完全隔离的气泡中运行,但这对应用程序的性能和内存使用有严重影响。创建容器实例的成本很高,并且第一次(每个容器)解析注册实例会导致生成表达式树、编译委托以及 JIT 编译它们。这甚至可能导致内存泄漏。

此外,这还意味着任何注册的单例类都将具有与任何作用域类相同的生命周期。状态无法再共享。

因此,我建议选择 3:

  • 仅使用一个 Container 实例并且不要调用BuildProvider多次
  • 创建一个ITenantId允许设置Id实例化后的实现
  • 将该实现注册为Scoped
  • 在每个 new 开始时IServiceScope,解析该实现并设置其 id。

这可能如下所示:

// Code
class TenentIdImpl : ITenantId
{
    public Guid Id { get; set; } // settable
}

// Startup:
services.AddScoped<TenentIdImpl>();
services.AddScoped<ITenantId>(c => c.GetRequiredService<TenantIdImpl>());

// In message pipeline
using (var scope = provider.CreateScope())
{
    var tenant = scope.ServiceProvider.GetRequiredService<TenantIdImpl>();
    tenant.Id = messageEnvelope.TenantId;

    var handler =
        scope.ServiceProvider.GetRequiredService<IMessageHandler<TMessage>>();

    handler.Handle(messageEnvelope.Message);
}
Run Code Online (Sandbox Code Playgroud)

这个特定的模型,您在对象图中存储状态,我在我的博客中对此进行了解释,我将其称为“闭包组合模型”