在每个请求上注册依赖服务

Register dependent services on every request

我在 Multi-tenant 解决方案中工作,主要有 2 种类型的应用程序

  1. 网络API
  2. 控制台应用程序处理来自 queue
  3. 的消息

我已经实现了依赖注入来注入所有服务。我已经创建了 TenantContext class,我在其中解析来自 HTTP header 的租户信息并且它在 API 中工作正常,但控制台应用程序通过每条消息获取租户信息(租户信息是queue 消息的一部分)所以我在每个不正确的传入消息上调用依赖注入注册方法,这里有任何 suggestion/solution 吗?

我在 API

中解析 ITenantContext 的方式
services.AddScoped<ITenantContext>(serviceProvider =>
{
    //Get Tenant from JWT token
    if (string.IsNullOrWhiteSpace(tenantId))
    {
        //1. Get HttpAccessor and processor settings
        var httpContextAccessor =
            serviceProvider.GetRequiredService<IHttpContextAccessor>();

        //2. Get tenant information (temporary code, we will get token from JWT)
        tenantId = httpContextAccessor?.HttpContext?.Request.Headers["tenant"]
            .FirstOrDefault();
        if (string.IsNullOrWhiteSpace(tenantId))
            //throw bad request for api
            throw new Exception($"Request header tenant is missing");
    }

    var tenantSettings =
        serviceProvider.GetRequiredService<IOptionsMonitor<TenantSettings>>();
    return new TenantContext(tenantId, tenantSettings );
});

创建两个不同的 ITenantContext 实现。一种用于 Web API,另一种用于控制台应用程序。

您的 Web API 实现可能如下所示:

public class WebApiTenantContext : ITenantContext
{
    private readonly IHttpContextAccessor accessor;
    private readonly IOptionsMonitor<TenantSettings> settings;

    public WebApiTenantContext(
        IHttpContextAccessor accessor,
        IOptionsMonitor<TenantSettings> settings)
    {
        // Notice how the dependencies are not used in this ctor; this is a best
        // practice. For more information about this, see Mark's blog:
        // https://blog.ploeh.dk/2011/03/03/InjectionConstructorsshouldbesimple/
        this.accessor = accessor;
        this.settings = settings;
    }

    // This property searches for the header each time its called. If needed,
    // it can be optimized by using some caching, e.g. using Lazy<string>.
    public string TenantId =>
        this.accessor.HttpContext?.Request.Headers["tenant"].FirstOrDefault()
        ?? throw new Exception($"Request header tenant is missing");
}

请注意,此实现对于您的目的来说可能有点幼稚,但希望您能理解。

这个class可以在WebAPI项目的Composition Root中注册如下:

services.AddScoped<ITenantContext, WebApiTenantContext>();

因为 WebApiTenantContext 的所有依赖项都在构造函数中定义,您可以在 ITenantContext 抽象和 WebApiTenantContext 实现之间做一个简单的映射。

但是,对于控制台应用程序,您需要一种非常不同的方法。 WebApiTenantContext,如上所示,目前是无状态的。它能够从其依赖项中提取所需的数据(即 TenantId)。这可能不适用于您的控制台应用程序。在这种情况下,您可能需要手动将队列中每条消息的执行包装在 IServiceScope 中,并在该请求的开头初始化 ConsoleTenantContext 。在那种情况下,ConsoleTenantContext 将仅如下所示:

public class ConsoleTenantContext : ITentantContext
{
    public string TenantId { get; set; }
}

在控制台应用程序的 Composition Root 中的某处,您必须从队列中提取消息(您可能已经拥有的逻辑),这就是您执行以下操作的地方:

var envelope = PullInFromQueue();

using (var scope = this.serviceProvider.CreateScope())
{
    // Initialize the tenant context
    var context = scope.ServiceProvider.GetRequiredService<ConsoleTenantContext>();
    content.TenantId = envelope.TenantId;

    // Forward the call to the message handler
    var handler = scope.ServiceProvider.GetRequiredService<IMessageHandler>();
    handler.Handle(envelope.Message);
}

Console 应用程序的 Composition Root 将如何进行以下注册:

services.AddScoped<ConsoleTenantContext>();
services.AddScoped<ITenentContext>(
    c => c.GetRequiredServices<ConsoleTenantContext>());

通过上面的注册,您将 ConsoleTenantContext 注册为作用域。这是必需的,因为之前的消息基础设施需要显式引入 ConsoleTenantContext 来配置它。但是应用程序的其余部分将依赖于 ITenantContext,这就是它也需要注册的原因。该注册只是将自身转发给已注册的 ConsoleTenantContext 以确保两个注册都指向同一范围内的同一实例。当有两个实例时,这将不起作用。

请注意,您可以对 Web API 使用与此处针对控制台应用程序所演示的相同的方法,但实际上,与这样做相比,干预 Web API 的请求生命周期更难使用您的控制台应用程序,您可以完全控制。这就是为什么在这种情况下,与从外部初始化的 ITenantContext 相比,使用本身负责检索正确值的 ITenantContext 实现对于 Web API 是一种更简单的解决方案。

您在这里看到的是您在配置应用程序时可以使用的不同组合模型的演示。我在我的博客 series on DI Composition Models 中详细介绍了这一点。