更新 10/6/2017: 我们发布了 Autofac.AspNetCore.Multitenant将解决方案封装在一个更易于使用的包中。我将把最初的答案/解释留在这里供后代使用,但如果你遇到了这个问题,你可以去拿那个包裹并继续前进。
我认为你遇到了时间问题。
如果您在中间件中的 HttpContext 上弹出打开调试器,您可以看到有一个RequestServicesFeature
属性上的对象称为ServiceProvidersFeature
. 这就是负责创建每个请求范围的内容。作用域是在第一次访问时创建的。
看起来顺序大致是这样的:
- The WebHostBuilder添加启动过滤器以允许将请求服务添加到管道中.
- 启动过滤器,AutoRequestServicesStartupFilter,将中间件添加到管道的最开始,以触发请求服务的创建。
- 添加的中间件,RequestServicesContainerMiddleware,基本上只是调用
RequestServices
财产来自ServiceProvidersFeature
触发每个请求生命周期范围的创建。然而,在它的构造函数中是它得到的地方IServiceScopeFactory它用来创建请求范围,这不是很好,因为它将从根容器创建在建立租户之前.
所有这些都会产生一种情况,即每个请求的范围已被确定为默认租户,并且您无法真正更改它。
要解决此问题,您需要自行设置请求服务,以便它们考虑多租户。
听起来比实际情况更糟。
首先,我们需要对应用程序容器的引用。我们需要能够从应用程序级服务而不是请求服务来解决某些问题。我通过添加一个来做到这一点static
财产给你的Startup
类并将容器保留在那里。
public static IContainer ApplicationContainer { get; private set; }
接下来,我们将更改您的中间件,使其看起来更像RequestServicesContainerMiddleware
.您需要设置HttpContext
首先,让您的租户 ID 策略发挥作用。之后,您可以获得IServiceScopeFactory
并遵循与他们相同的模式RequestServicesContainerMiddleware
.
public class RequestMiddleware
{
private static readonly AsyncLocal<HttpContext> _context = new AsyncLocal<HttpContext>();
private readonly RequestDelegate _next;
public RequestMiddleware(RequestDelegate next)
{
this._next = next;
}
public static HttpContext Context => _context.Value;
public async Task Invoke(HttpContext context)
{
_context.Value = context;
var existingFeature = context.Features.Get<IServiceProvidersFeature>();
using (var feature = new RequestServicesFeature(Startup.ApplicationContainer.Resolve<IServiceScopeFactory>()))
{
try
{
context.Features.Set<IServiceProvidersFeature>(feature);
await this._next.Invoke(context);
}
finally
{
context.Features.Set(existingFeature);
_context.Value = null;
}
}
}
}
现在您需要一个启动过滤器来将中间件放入其中。您需要一个启动过滤器,否则RequestServicesContainerMiddleware
将在管道中运行得太早,并且事情已经从错误的租户范围开始解决。
public class RequestStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<RequestMiddleware>();
next(builder);
};
}
}
将启动过滤器添加到服务集合的最开始处。您需要先运行启动过滤器AutoRequestServicesStartupFilter
.
The ConfigureServices
最终看起来像这样:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.Insert(0, new ServiceDescriptor(typeof(IStartupFilter), typeof(RequestStartupFilter), ServiceLifetime.Transient));
services.AddMvc();
var builder = new ContainerBuilder();
builder.RegisterType<TestMultitenancyContext>().InstancePerTenant();
builder.Populate(services);
var container = new MultitenantContainer(new MultitenantIdentificationStrategy(), builder.Build());
ApplicationContainer = container;
return new AutofacServiceProvider(container);
}
请注意Insert
在那里调用以将您的服务注册堵塞在顶部,在其启动过滤器之前。
新的操作顺序将是:
- At app startup...
- 您的启动过滤器会将您的自定义请求服务中间件添加到管道中。
- The
AutoRequestServicesStartupFilter
将添加RequestServicesContainerMiddleware
到管道。
- During a request...
- 您的自定义请求中间件将根据入站请求信息设置请求服务。
- The
RequestServicesContainerMiddleware
将看到请求服务已经设置并且不会执行任何操作。
- 解析服务后,请求服务范围将已经是自定义请求中间件设置的租户范围,并且将显示正确的内容。
我通过将租户 ID 切换为来自查询字符串而不是主机名(因此我不必设置主机文件条目和所有这些爵士乐)在本地进行了测试,并且我能够通过切换查询字符串参数来切换租户。
现在,您也许可以稍微简化一下。例如,您可以通过直接对 Web 主机构建器执行某些操作来摆脱启动过滤器Program
班级。您也许可以直接使用以下命令注册您的启动过滤器ContainerBuilder
打电话之前builder.Populate
并跳过那个Insert
称呼。您也许可以存储IServiceProvider
in the Startup
类属性(如果您不喜欢 Autofac 在系统中传播)。如果您自己创建中间件实例并将容器作为构造函数参数传入,那么您可能无需静态容器属性即可摆脱困境。不幸的是,我已经花了很多时间试图找出解决方法,所以我不得不把“优化它”作为读者的练习。
再次抱歉,这并不清楚。我已经代表您提出了一个问题,以更新文档,并可能找到一种更简单的更好方法来做到这一点。