Autofac 多租户应用
Autofac.Multitenant
提供了多租户依赖注入支持。(在 v4.0.0 之前,该包的名称为 Autofac.Extras.Multitenant
。)
什么是多租户
多租户应用 是一种部署一次但允许多个客户(或“租户”)以自己的方式访问应用的应用程序。
例如,考虑一个托管的在线商店应用——你作为“租户”,租赁该应用,设置一些配置值。当用户使用自定义域名访问应用时,它看起来就像你的公司。其他租户也可能租赁该应用,但应用只在一个中央托管服务器上部署一次,并根据访问它的租户(或租户的最终用户)进行行为调整。
多租户环境中的许多更改都是通过简单的配置来完成的。例如,UI 显示的颜色或字体是可以通过简单配置插件实现的,而无需实际改变应用的行为。
在更复杂的场景中,你可能需要根据租户进行业务逻辑的更改。 例如,特定租户可能希望使用一些复杂自定义逻辑来更改某个值的计算方式。如何为应用注册默认行为/依赖项,并允许特定租户覆盖它?
这就是 Autofac.Multitenant
力求解决的问题。
一般原则
一般来说,多租户应用在依赖解析方面需要执行以下四个任务:
- 引用 NuGet 包
- 注册依赖项
- 租户识别
- 解析依赖项
本节概述了这四个步骤的工作原理。后续章节将扩展这些主题,包括如何将这些原则与特定应用类型集成。
引用 NuGet 包
任何想要使用多租户功能的应用都需要添加对以下 NuGet 包的引用...
- Autofac
- Autofac.Multitenant
对于 WCF 应用,还需要 Autofac.Multitenant.Wcf
。
注册依赖项
Autofac.Multitenant
引入了一个名为 Autofac.Multitenant.MultitenantContainer
的新容器类型。这个容器用于管理应用级别的默认设置和特定租户的覆盖。
整体注册过程如下:
- 创建应用级别的默认容器。 这个容器用于注册应用的默认依赖项。如果租户没有为依赖项类型提供其他覆盖,则这里注册的依赖项将被使用。
- 实例化租户标识策略。 租户标识策略用于根据执行上下文确定当前租户的 ID。关于这一点稍后会详细介绍。
- 创建多租户容器。 多租户容器负责跟踪应用默认设置和特定租户的覆盖。
- 注册特定租户的覆盖。 对于每个希望覆盖依赖项的租户,通过传递租户 ID 和配置 Lambda 注册适当的覆盖。
通用用法如下所示:
// 首先,使用标准 ContainerBuilder 创建应用级别的默认容器,就像你习惯的那样。
var builder = new ContainerBuilder();
builder.RegisterType<Consumer>().As<IDependencyConsumer>().InstancePerDependency();
builder.RegisterType<BaseDependency>().As<IDependency>().SingleInstance();
var appContainer = builder.Build();
// 在构建应用级别的默认容器后,你需要创建一个租户标识策略。详细信息稍后会介绍。
var tenantIdentifier = new MyTenantIdentificationStrategy();
// 现在使用应用容器和租户标识策略创建多租户容器。
var mtc = new MultitenantContainer(tenantIdentifier, appContainer);
// 通过传递租户 ID 和一个接受 ContainerBuilder 的 Lambda 来配置每个租户的覆盖。
mtc.ConfigureTenant('1', b => b.RegisterType<Tenant1Dependency>().As<IDependency>().InstancePerDependency());
mtc.ConfigureTenant('2', b => b.RegisterType<Tenant2Dependency>().As<IDependency>().SingleInstance());
// 现在你可以使用多租户容器来解析实例。
如果你有一个需要为每个租户提供一个实例的组件, 可以在容器级别使用 InstancePerTenant()
注册扩展方法。
var builder = new ContainerBuilder();
builder.RegisterType<SomeType>().As<ISomeInterface>().InstancePerTenant();
// InstancePerTenant 应放在主容器中;其他特定租户的依赖项按照上面所示,在特定租户的生命周期中注册。
注意,你只能为租户配置一次。 之后,你不能再更改租户的覆盖。此外,如果为租户解析依赖项,他们的生命周期范围可能不会更改。在应用启动时配置租户覆盖是一个好习惯,以避免出现任何问题。如果需要根据业务逻辑“构建”租户配置,可以使用 Autofac.Multitenant.ConfigurationActionBuilder
。
var builder = new ContainerBuilder();
// ... 注册内容...
var appContainer = builder.Build();
var tenantIdentifier = new MyTenantIdentificationStrategy();
var mtc = new MultitenantContainer(tenantIdentifier, appContainer);
// 创建一个配置动作构建器,以便在业务逻辑过程中累积注册操作。
var actionBuilder = new ConfigurationActionBuilder();
// 执行一些逻辑...
if(SomethingIsTrue())
{
actionBuilder.Add(b => b.RegisterType<AnOverride>().As<ISomething>());
}
actionBuilder.Add(b => b.RegisterType<SomeClass>());
if(AnotherThingIsTrue())
{
actionBuilder.Add(b => b.RegisterModule<MyModule>());
}
// 现在使用构建的动作配置一个租户。
mtc.ConfigureTenant('1', actionBuilder.Build());
识别租户
为了解析特定租户的依赖项,Autofac 需要知道哪个租户正在发出解析请求。也就是说,“对于当前执行上下文,哪个租户正在解析依赖项?”
Autofac.Multitenant
包含一个 ITenantIdentificationStrategy
接口,你可以实现它来提供这样的机制。这允许你在应用程序中适当的地方获取租户 ID:环境变量、当前用户角色、传入请求值,或其他任何地方。
以下是一个简单的 Web 应用可能使用的示例 ITenantIdentificationStrategy
。
using System;
using System.Web;
using Autofac.Multitenant;
namespace DemoNamespace
{
// 警告:这只是一个示例,不推荐使用。
// * 没有太多的错误检查,不健壮。
// * 这真的只是示例。
public class RequestParameterStrategy : ITenantIdentificationStrategy
{
public bool TryIdentifyTenant(out object tenantId)
{
tenantId = null;
try
{
var context = HttpContext.Current;
if(context != null && context.Request != null)
{
tenantId = context.Request.Params["tenant"];
}
}
catch(HttpException)
{
// IIS 7.0 启动时发生
}
return tenantId != null;
}
}
}
在这个示例中,一个 Web 应用程序使用传入的请求参数来获取租户 ID。(请注意,这只是示例,不推荐使用,因为它会允许系统上的任何用户非常容易地切换租户。它也没有处理发生在 Web 请求之外的情况。)
在自定义策略实现中,你可能会选择将租户 ID 表示为 GUID、整数或其他自定义类型。这里的策略是将值从执行上下文解析为强类型对象,并根据值是否存在以及是否可以解析为适当类型来成功/失败。
Autofac.Multitenant
使用 System.Object
作为整个系统的租户 ID 类型,以最大限度地提高灵活性。
在租户识别中性能很重要。 每次解析组件、开始新的生命周期范围等时都会进行租户识别。因此,确保租户识别策略快速非常重要。例如,你不希望在租户识别期间进行服务调用或数据库查询。
在租户识别中要妥善处理错误。 特别是在 ASP.NET 应用程序启动等情况下,你可能使用某种上下文机制(如 HttpContext.Current.Request
)来确定租户 ID,但如果在租户 ID 策略被调用时该上下文信息不可用,你需要能够处理这种情况。在上面的示例中,它不仅检查当前 HttpContext
,还检查 Request
。检查所有内容并处理异常(如解析异常),否则你可能会遇到难以调试的奇怪行为。
对于你的租户 ID 策略还有更多提示,请参阅 tenant_id_strategy_tips
部分。
解析特定租户的依赖项
MultitenantContainer
工作方式是,系统中的每个租户都有自己的 Autofac.ILifetimeScope
实例,其中包含应用默认设置和特定租户的覆盖。这样做...
var builder = new ContainerBuilder();
builder.RegisterType<BaseDependency>().As<IDependency>().SingleInstance();
var appContainer = builder.Build();
var tenantIdentifier = new MyTenantIdentificationStrategy();
var mtc = new MultitenantContainer(tenantIdentifier, appContainer);
mtc.ConfigureTenant('1', b => b.RegisterType<Tenant1Dependency>().As<IDependency>().InstancePerDependency());
这非常类似于使用标准的 ILifetimeScope.BeginLifetimeScope(Action<ContainerBuilder>)
,如下所示:
var builder = new ContainerBuilder();
builder.RegisterType<BaseDependency>().As<IDependency>().SingleInstance();
var appContainer = builder.Build();
using(var scope = appContainer.BeginLifetimeScope(
b => b.RegisterType<Tenant1Dependency>().As<IDependency>().InstancePerDependency())
{
// 在创建的范围中进行工作...
}
当你使用 MultitenantContainer
来解析依赖项时,它会在幕后调用你的 ITenantIdentificationStrategy
来识别租户,找到具有配置覆盖的租户的生命周期范围,并从该范围中解析依赖项。所有这些都在透明的方式下完成,所以你可以像使用其他容器一样使用多租户容器。
var dependency = mtc.Resolve<IDependency>();
// "dependency"将是从多租户容器中根据当前租户特定值解析的。如果当前租户已经覆盖了IDependency注册,则会解析覆盖项;否则,它将是应用程序级别的默认值。
这里的关键在于,所有的工作都是在幕后透明地进行的。对 Resolve
、BeginLifetimeScope
、Tag
、Disposer
或 IContainer
接口上的其他方法/属性的任何调用都会经过租户识别过程,并且结果将是特定于租户的。
如果你需要直接访问租户的生命周期范围或应用程序容器,MultitenantContainer
提供了以下功能:
ApplicationContainer
:获取应用程序容器。GetCurrentTenantScope
:识别当前租户并返回他们的特定生命周期范围。GetTenantScope
:允许你提供一个特定的租户 ID,以便获取该租户的生命周期范围。
ASP.NET 集成
ASP.NET 集成与其他 标准 ASP.NET 应用集成 并没有太大不同。真正不同的是,你需要设置应用程序的 Autofac.Integration.Web.IContainerProvider
或 System.Web.Mvc.IDependencyResolver
或其他什么,使用 Autofac.Multitenant.MultitenantContainer
而不是由 ContainerBuilder
构建的标准容器。由于 MultitenantContainer
以透明的方式处理多租户,所以 “一切正常” 。
ASP.NET 应用程序启动
以下是一个 ASP.NET MVC 的 Global.asax
示例,说明了它是多么简单:
namespace MultitenantExample.MvcApplication
{
public class MvcApplication : HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
// 注册路由 - 标准MVC内容。
}
protected void Application_Start()
{
// 设置租户ID策略和应用程序容器。这里使用请求参数租户ID策略作为示例。你应该在生产环境中使用自己的策略。
var tenantIdStrategy = new RequestParameterTenantIdentificationStrategy("tenant");
var builder = new ContainerBuilder();
builder.RegisterType<BaseDependency>().As<IDependency>();
// 如果你的应用程序同一个命名空间中有特定于租户的控制器,你应该分别注册控制器。
builder.RegisterType<HomeController>();
// 创建多租户容器和租户覆盖。
var mtc = new MultitenantContainer(tenantIdStrategy, builder.Build());
mtc.ConfigureTenant("1",
b =>
{
b.RegisterType<Tenant1Dependency>().As<IDependency>().InstancePerDependency();
b.RegisterType<Tenant1Controller>().As<HomeController>();
});
// 这是神奇的一行:使用多租户容器而不是常规容器设置DependencyResolver。
DependencyResolver.SetResolver(new AutofacDependencyResolver(mtc));
// ...其他一切都是标准MVC。
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
}
}
如你所见,它几乎就像常规 MVC Autofac 集成一样。你设置应用程序容器、租户 ID 策略、多租户容器和租户覆盖,就像前面在 register_dependencies
和 tenant_identification
中所示。然后当你设置 DependencyResolver
时,给它一个多租户容器。其他一切都正常工作。
对于其他 Web 应用,这种相似性仍然适用。当设置 Web 表单的 IContainerProviderAccessor
时,使用多租户容器而不是标准容器。当设置 Web API 的 DependencyResolver
时,使用多租户容器而不是标准容器。
请注意,在示例中,控制器是分开注册的,而不是使用一次注册所有控制器的 builder.RegisterControllers(Assembly.GetExecutingAssembly());
风格。请参阅下面的内容了解原因。
特定于租户的控制器
在 MVC 应用中,你可能会选择让租户覆盖控制器。这是可能的,但需要一些考虑。
首先,**特定于租户的控制器必须继承它们要覆盖的控制器。**例如,如果你有一个 HomeController
,而一个租户想要创建他们自己的实现,他们需要从它派生,如下所示...
public class Tenant1HomeController : HomeController
{
// 租户特定的控制器实现。
}
其次,如果你的应用程序控制器和特定于租户的控制器在同一命名空间中,你不能一次性注册所有的控制器。你可能在标准 ASP.NET MVC 集成 中看到过一行 builder.RegisterControllers(Assembly.GetExecutingAssembly());
,用来一次性注册命名空间中的所有控制器。不幸的是,如果在这个命名空间中有特定于租户的控制器,那么如果你这样做,它们都将被注册为应用程序级别。相反,你需要逐个注册每个应用程序控制器,然后以相同的方式配置租户特定的覆盖。
上面的 Global.asax
示例展示了这种逐个注册控制器的模式。
当然,如果你将特定于租户的控制器保存在其他命名空间中,你可以一次性使用 builder.RegisterControllers(Assembly.GetExecutingAssembly());
注册所有应用程序控制器,而且它会正常工作。请注意,如果特定于租户的控制器库没有被主应用程序引用(例如,它们是动态注册的插件,使用装配探测或其他方式)你需要使用 ASP.NET BuildManager
来注册它们。
最后,当注册特定于租户的控制器时,应将它们“注册为”基控制器类型。在上面的示例中,可以看到默认控制器在应用程序容器中像这样注册:
var builder = new ContainerBuilder();
builder.RegisterType<HomeController>();
然后当租户在他们的租户配置中覆盖控制器时,看起来像这样:
var mtc = new MultitenantContainer(tenantIdStrategy, builder.Build());
mtc.ConfigureTenant("1", b => b.RegisterType<Tenant1Controller>().As<HomeController>());
由于这个相对复杂性,可能更好的做法是将业务逻辑隔离到外部依赖项中,这些依赖项会传递到控制器,以便租户可以提供替代依赖项,而不是覆盖控制器。
ASP.NET Core 集成
ASP.NET Core 改变了许多东西。多租户集成已在 我们的 ASP.NET Core 集成页面上进行了概述 。
WCF 集成
WCF 集成与 标准 WCF 集成 稍有不同,因为你需要使用不同的服务主机工厂,而且需要一些额外的配置。
此外,识别租户也更难——客户端需要以某种方式向服务传递租户 ID,而服务需要知道如何解释传递的租户 ID。Autofac.Multitenant
为此提供了一个简单的解决方案,即在消息头中传递相关信息的行为。
WCF 集成的参考包
对于消费多租户服务(客户端应用),请添加引用到...
- Autofac
- Autofac.Multitenant
对于提供多租户服务(服务应用),请添加引用到...
- Autofac
- Autofac.Integration.Wcf
- Autofac.Multitenant
- Autofac.Multitenant.Wcf
通过行为传递租户 ID
如前所述(tenant_identification
),为了使多租户工作,你必须确定哪个租户正在执行某个调用,以便你可以解析适当的依赖项。在服务环境中,一个挑战是租户通常在客户端应用程序端建立,而需要将租户 ID 传播到服务,以便服务能够正确行事。
解决这个问题的一个常见方案是在消息头中传播租户 ID。客户端在发出的消息中添加一个特殊的头,其中包含租户 ID。服务解析该头,读取租户 ID,然后使用该 ID 来确定其功能。
在 WCF 中,将这些“动态”头附加到消息并在接收时读取它们的方式是通过行为。你在客户端和服务端都应用这种行为,以便使用相同的头信息(类型、URN 等)。
Autofac.Multitenant
提供了简单的租户 ID 传播行为,即 Autofac.Multitenant.Wcf.TenantPropagationBehavior
。在客户端侧使用时,它使用租户 ID 策略检索上下文租户 ID,并将其插入到传出消息的消息头中。在服务器端使用时,它查找此入站头并解析租户 ID,将其放入操作上下文扩展中。
wcf_startup
部分显示了在客户端和服务端应用此行为的示例。
如果你使用此行为,将提供一个相应的服务器端租户识别策略。请参阅 operationcontext_id
,下面的内容。
从 OperationContext 获取租户标识
无论你选择是否使用提供的 Autofac.Multitenant.Wcf.TenantPropagationBehavior
在消息头中从客户端传播到服务器(参见上方的 behavior_id
),在整个操作生命周期中存储租户 ID 的好地方是 OperationContext
。
Autofac.Multitenant.Wcf
为此提供了 Autofac.Multitenant.Wcf.TenantIdentificationContextExtension
,作为 WCF OperationContext
的扩展。
在操作生命周期的早期阶段(通常在 System.ServiceModel.Dispatcher.IDispatchMessageInspector.AfterReceiveRequest()
实现中),你可以将 TenantIdentificationContextExtension
添加到当前 OperationContext
,以便轻松识别租户。下面的示例 AfterReceiveRequest()
实现展示了这一点:
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
// 假设租户ID来自消息头;你可以从任何地方获取它。
var tenantId = request.Headers.GetHeader<TTenantId>(TenantHeaderName, TenantHeaderNamespace);
// 这是添加上下文扩展的地方:
OperationContext.Current.Extensions.Add(new TenantIdentificationContextExtension() { TenantId = tenantId });
return null;
}
一旦将租户 ID 附加到上下文,你可以根据需要使用适当的 ITenantIdentificationStrategy
来检索它。
如果你使用 TenantIdentificationContextExtension
,那么提供的 Autofac.Multitenant.Wcf.OperationContextTenantIdentificationStrategy
会自动工作,从 OperationContext
中获取租户 ID。
部署多租户服务
在 WCF 服务应用程序中,服务实现可能是特定于租户的,但共享相同的 Service Contract。这允许你将服务合同分离到特定于租户的开发人员可使用的单独程序集中,并允许他们实现自定义逻辑,而无需共享默认实现的任何内部细节。
为了实现这一点,已经实现了多租户服务定位的自定义策略:Autofac.Multitenant.Wcf.MultitenantServiceImplementationDataProvider
。
在你的服务的 .svc
文件中,必须指定以下内容:
- **服务合同接口的完整类型名称。**在常规 WCF 集成 中,Autofac 允许您使用类型或命名服务。对于多租户,您必须使用基于服务合同接口的类型服务。
- **Autofac 主机工厂的完整类型名称。**这可以让托管环境知道要使用哪个工厂。(这就像标准的 Autofac WCF 集成 一样。)
一个示例 .svc
文件如下所示:
<%@ ServiceHost
Service="MultitenantExample.WcfService.IMultitenantService, MultitenantExample.WcfService"
Factory="Autofac.Integration.Wcf.AutofacServiceHostFactory, Autofac.Integration.Wcf" %>
当使用 Autofac 容器注册服务实现时,必须像这样将实现注册为合同接口:
builder.RegisterType<BaseImplementation>().As<IMultitenantService>();
然后,可以使用接口类型注册特定于租户的实现:
mtc.ConfigureTenant("1", b => b.RegisterType<Tenant1Implementation>().As<IMultitenantService>());
别忘了在应用程序启动时,在设置容器的附近,你需要告诉 Autofac 你在做多租户:
AutofacHostFactory.ServiceImplementationDataProvider =
new MultitenantServiceImplementationDataProvider();
管理服务属性
在 XML 配置(如 web.config)中配置 WCF 服务时,WCF 会自动推断期望的服务元素名称,该名称来自具体的实现类型。例如,在单租户实现中,你的 MyNamespace.IMyService
服务接口可能有一个名为 MyNamespace.MyService
的实现,这就是 WCF 在 web.config
中期望查找的:
<system.serviceModel>
<services>
<service name="MyNamespace.MyService" />
</services>
</system.serviceModel>
然而,使用多租户服务主机时,实现接口的具体服务类型是一个动态生成的代理类型,因此服务配置名称变为自动生成的类型名称,如下所示:
<system.serviceModel>
<services>
<service name="Castle.Proxies.IMyService_1" />
</services>
</system.serviceModel>
为了简化这个过程,Autofac.Multitenant.Wcf
提供了 Autofac.Multitenant.Wcf.ServiceMetadataTypeAttribute
,您可以使用它来创建一个“元数据伙伴类”(类似于 System.ComponentModel.DataAnnotations.MetadataTypeAttribute
),并标记它以添加类型级别的元数据,从而修改动态代理的行为。
在这种情况下,你需要动态代理具有 System.ServiceModel.ServiceBehaviorAttribute
,以便定义预期的 ConfigurationName
。
首先,将服务接口标记为 ServiceMetadataTypeAttribute
:
using System;
using System.ServiceModel;
using Autofac.Multitenant.Wcf;
namespace MyNamespace
{
[ServiceContract]
[ServiceMetadataType(typeof(MyServiceBuddyClass))]
public interface IMyService
{
// ...定义你的服务操作...
}
}
接下来,创建在属性中指定的伙伴类,并添加适当的元数据。
using System;
using System.ServiceModel;
namespace MyNamespace
{
[ServiceBehavior(ConfigurationName = "MyNamespace.IMyService")]
public class MyServiceBuddyClass
{
}
}
现在,你可以在 XML 配置文件中使用在伙伴类上指定的配置名称:
<system.serviceModel>
<services>
<service name="MyNamespace.IMyService" />
</services>
</system.serviceModel>
关于元数据的重要注意事项:
- **只有类型级别的元数据被复制。**目前,仅复制伙伴类上的类型级别的元数据到动态代理。如果您有在属性级别或方法级别复制元数据的需求,请提交问题。- **并非所有元数据都会产生预期的效果。**例如,如果您使用
ServiceBehaviorAttribute
来定义与寿命相关的信息,如InstanceContextMode
,则服务不会遵循该指令,因为 Autofac 正在管理寿命,而不是标准的服务主机。指定元数据时要谨慎——如果不起作用,别忘了你没有使用标准的服务寿命管理功能。- **元数据是应用程序级的,不是按租户的。**元数据伙伴类的信息将在应用程序级别生效,不能被每个租户覆盖。
特定于租户的服务实现
如果你正在部署多租户服务( hosting
),你可以提供特定于租户的服务实现。这允许你提供服务的基础实现,并与租户共享服务合同,以便他们可以开发自定义服务实现。
**你必须将服务合同实现为单独的接口。**你不能将服务实现标记为 ServiceContractAttribute
。然后,服务实现必须实现该接口。这通常也是好习惯,但是多租户服务主机不允许直接由具体类型定义合同。
特定于租户的服务实现不需要从基础实现派生;它们只需要实现服务接口。
你可以在应用程序启动(见 wcf_startup
)时注册特定于租户的服务实现。
WCF 应用程序启动
应用程序启动通常与其他多租户应用程序( register_dependencies
)相同,但对客户端有一些小事情要做,对服务有一些主机设置。
WCF 客户端应用程序启动
在 WCF 客户端应用程序中,当你注册服务客户端时,你需要注册将租户 ID 传播到服务的行为。如果你遵循 标准 WCF 集成指南 ,那么注册服务客户端看起来像这样:
// 创建客户端应用程序的租户ID策略。
var tenantIdStrategy = new MyTenantIdentificationStrategy();
// 注册应用程序级别的依赖项。
var builder = new ContainerBuilder();
builder.RegisterType<BaseDependency>().As<IDependency>();
// 服务客户端本身不随租户变化,因为服务本身就是多租户——所有租户共享一个客户端和**服务实现**。
builder.Register(c =>
new ChannelFactory<IMultitenantService>(
new BasicHttpBinding(),
new EndpointAddress("http://server/MultitenantService.svc"))).SingleInstance();
// 在客户端通道工厂上注册一个端点行为,该行为将在消息头中跨线程传播租户ID。
// 在此示例中,使用内置的`TenantPropagationBehavior`将字符串形式的租户ID发送到网络上。
builder.Register(c =>
{
var factory = c.Resolve<ChannelFactory<IMultitenantService>>();
factory.Opening += (sender, args) => factory.Endpoint.Behaviors.Add(new TenantPropagationBehavior<string>(tenantIdStrategy));
return factory.CreateChannel();
});
// 创建多租户容器。
var mtc = new MultitenantContainer(tenantIdStrategy, builder.Build());
// ...注册租户重载等。
WCF 服务应用程序启动
在 WCF 服务应用程序 中,您注册默认值和租户特定的覆盖项,就像通常一样(register_dependencies
),但还需要:
- 设置服务主机的行为以期待接收一个用于识别租户的头信息(
behavior_id
)。 - 将服务主机工厂容器设置为
MultitenantContainer
。
下面的示例中,我们使用的是 Autofac.Multitenant.Wcf.AutofacHostFactory 而不是标准的 Autofac 容器工厂(如前面所述)。
namespace MultitenantExample.WcfService
{
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
// 创建租户标识策略。
var tenantIdStrategy = new OperationContextTenantIdentificationStrategy();
// 注册应用程序级依赖项和服务实现。
var builder = new ContainerBuilder();
builder.RegisterType<BaseImplementation>().As<IMultitenantService>();
builder.RegisterType<BaseDependency>().As<IDependency>();
// 创建多租户容器。
var mtc = new MultitenantContainer(tenantIdStrategy, builder.Build());
// 注意我们在下面配置租户 ID 为字符串,因为租户标识策略从消息头中检索字符串值。
// 配置租户 1 的覆盖项 - 依赖项、服务实现等。
mtc.ConfigureTenant("1",
b =>
{
b.RegisterType<Tenant1Dependency>().As<IDependency>().InstancePerDependency();
b.RegisterType<Tenant1Implementation>().As<IMultitenantService>();
});
// 向创建的服务主机添加行为,以便检查传入的消息并从中解析租户 ID。
AutofacHostFactory.HostConfigurationAction =
host =>
host.Opening += (s, args) =>
host.Description.Behaviors.Add(new TenantPropagationBehavior<string>(tenantIdStrategy));
// 将服务实现策略设置为多租户。
AutofacHostFactory.ServiceImplementationDataProvider =
new MultitenantServiceImplementationDataProvider();
// 最后,将多租户 WCF 主机的主机工厂应用程序容器设置为多租户容器。
AutofacHostFactory.Container = mtc;
}
}
}
租户 ID 策略提示
- 性能是关键。 租户 ID 策略将在多租户容器中的每个解决操作和每个生命周期范围创建时执行。尽可能使其高效 - 每次都缓存而不是进行数据库查找,减少内存分配等。
- 妥善处理错误。 如果由于任何原因租户 ID 策略出现问题,可能很难调试。确保检查空指针并处理异常。从性能角度来看,确保使用
TryGet
或TryParse
类似的操作,而不是try/catch
并让异常控制流程。 - 使租户 ID 策略成为单例。 多租户容器存储了租户 ID 策略的实例。如果在基(非多租户)容器中注册该策略,请确保将其注册为单例。此外,确保租户 ID 策略可能消耗的依赖项也是单例... 或允许它们具有单独的实例缓存。
- 如果可以,先创建租户 ID 策略,然后注册它,而不是简单地注册类型到 Autofac 并让它解决。很诱人“过度 DI”。租户 ID 策略相当基础,您希望确保它正常工作;调试为什么某些内容没有注入到策略可能会很痛苦。此外,很容易“意外”地使租户 ID 策略成为工厂范围或生命周期范围的东西,这将不起作用。还容易忽略事实,即放入租户 ID 策略中的内容在整个应用程序的生命周期内被缓存,而不是针对每个请求进行填充。如果您实际上“新创建”策略并将其实例与 Autofac 注册,而不是注册单例类型,这可以强化这种纪律并帮助您避免问题。
- 注意线程问题! 租户 ID 策略由多租户容器持有,并跨所有操作使用。它是单例!如果缓存内容,请确保缓存操作是线程安全的。如果存储状态,请确保使用字典之类的结构,其中可以为系统中的每个租户存储状态(或以不依赖于租户的方式缓存查找)。如果您有一个名为 "tenant" 的实例变量,您会遇到麻烦 - 这个 "tenant" 在每个线程和每个解决操作中都是不同的。
示例
Autofac 示例仓库有一个 多租户 WCF 服务 和 相关的 MVC 客户端应用程序,展示了如何 [多租户服务托管 <../advanced/multitenant>](https://github.com/autofac/Examples/tree/master/src/MultitenantExample.MvcApplication)的工作。
还有一个非常简单的 控制台应用程序示例。