多租户

ABP的多租户模块提供了创建多租户应用程序的基本功能.

维基百科中是这样定义多租户的:

软件多租户技术指的是一种软件架构,这种架构可以使用软件的单实例运行并为多个租户提供服务.租户是通过软件实例的特定权限共享通用访问的一组用户.使用多租户架构,软件应用为每个租户提供实例的专用共享,包括实例的数据、配置、用户管理、租户的私有功能和非功能属性.多租户与多实例架构形成对比,将软件实例的行为根据不同的租户分割开来.

Volo.Abp.MultiTenancy

Volo.Abp.MultiTenancy”multi-tenancy ready”,使用包管理器控制台(PMC)将它安装到你的项目中:

  1. Install-Package Volo.Abp.MultiTenancy

这个包默认安装在了快速启动模板中.所以,大多数情况下,你不需要手动安装它.

然后你可以添加 AbpMultiTenancyModule 依赖到你的模块:

  1. using Volo.Abp.Modularity;
  2. using Volo.Abp.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. [DependsOn(typeof(AbpMultiTenancyModule))]
  6. public class MyModule : AbpModule
  7. {
  8. //...
  9. }
  10. }

随着”Multi-tenancy ready”的概念,我们打算开发我们的代码和多租户方法兼容.然后它可以被用于多租户和非多租户的程序中,这取决于最终程序的需求.

AbpMultiTenancyOptions: 处理不活跃或不存在的租户

MultiTenancyMiddlewareErrorPageBuilderAbpMultiTenancyOptions 用于 处理不活跃或不存在的租户.

默认情况下会响应错误页面, 你可以根据自己的需要更改它, 比如: 只输出错误日志并继续ASP NET Core的请求管道

  1. Configure<AbpMultiTenancyOptions>(options =>
  2. {
  3. options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) =>
  4. {
  5. // Handle the exception.
  6. };
  7. });

定义实体

你可以在你的实体中实现 IMultiTenant 接口来实现多租户,例如:

  1. using System;
  2. using Volo.Abp.Domain.Entities;
  3. using Volo.Abp.MultiTenancy;
  4. namespace MyCompany.MyProject
  5. {
  6. public class Product : AggregateRoot, IMultiTenant
  7. {
  8. public Guid? TenantId { get; set; } //IMultiTenant 定义了 TenantId 属性
  9. public string Name { get; set; }
  10. public float Price { get; set; }
  11. }
  12. }

实现IMultiTenant接口,需要在实体中定义一个 TenantId 的属性(查看更多有关实体的文档)

获取当前租户的Id

你的代码中可能需要获取当前租户的Id(先不管它具体是怎么取得的).对于这种情况你可以注入并使用 ICurrentTenant 接口.例如:

  1. using Volo.Abp.DependencyInjection;
  2. using Volo.Abp.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. public class MyService : ITransientDependency
  6. {
  7. private readonly ICurrentTenant _currentTenant;
  8. public MyService(ICurrentTenant currentTenant)
  9. {
  10. _currentTenant = currentTenant;
  11. }
  12. public void DoIt()
  13. {
  14. var tenantId = _currentTenant.Id;
  15. //在你的代码中使用tenantId
  16. }
  17. }
  18. }

改变当前租户

TODO: …

确定当前租户

多租户的应用程序运行的时候首先要做的就是确定当前租户. Volo.Abp.MultiTenancy只提供了用于确定当前租户的抽象(称为租户解析器),但是并没有现成的实现.

Volo.Abp.AspNetCore.MultiTenancy已经实现了从当前Web请求(从子域名,请求头,cookie,路由…等)中确定当前租户.本文后面会介绍Volo.Abp.AspNetCore.MultiTenancy.

自定义租户解析器

你可以像下面这样,在你模块的ConfigureServices方法中将自定义解析器并添加到 AbpTenantResolveOptions中:

  1. using Microsoft.Extensions.DependencyInjection;
  2. using Volo.Abp.Modularity;
  3. using Volo.Abp.MultiTenancy;
  4. namespace MyCompany.MyProject
  5. {
  6. [DependsOn(typeof(AbpMultiTenancyModule))]
  7. public class MyModule : AbpModule
  8. {
  9. public override void ConfigureServices(ServiceConfigurationContext context)
  10. {
  11. Configure<AbpTenantResolveOptions>(options =>
  12. {
  13. options.TenantResolvers.Add(new MyCustomTenantResolveContributor());
  14. });
  15. //...
  16. }
  17. }
  18. }

MyCustomTenantResolveContributor必须像下面这样实现ITenantResolveContributor接口:

  1. using System.Threading.Tasks;
  2. using Volo.Abp.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. public class MyCustomTenantResolveContributor : ITenantResolveContributor
  6. {
  7. public override Task ResolveAsync(ITenantResolveContext context)
  8. {
  9. context.TenantIdOrName = ... //从其他地方获取租户id或租户名字...
  10. }
  11. }
  12. }

如果能确定租户id或租户名字可以在租户解析器中设置 TenantIdOrName.如果不能确定,那就空着让下一个解析器来确定它.

租户存储

Volo.Abp.MultiTenancy中定义了 ITenantStore 从框架中抽象数据源.你可以实现ITenantStore,让它跟任何存储你租户的数据源(例如关系型数据库)一起工作.

配置数据存储

有一个内置的(默认的)租户存储,叫ConfigurationTenantStore.它可以被用于存储租户,通过标准的配置系统(使用Microsoft.Extensions.Configuration).因此,你可以通过硬编码或者在appsettings.json文件中定义租户.

例子:硬编码定义租户
  1. using System;
  2. using Microsoft.Extensions.DependencyInjection;
  3. using Volo.Abp.Data;
  4. using Volo.Abp.Modularity;
  5. using Volo.Abp.MultiTenancy;
  6. namespace MyCompany.MyProject
  7. {
  8. [DependsOn(typeof(AbpMultiTenancyModule))]
  9. public class MyModule : AbpModule
  10. {
  11. public override void ConfigureServices(ServiceConfigurationContext context)
  12. {
  13. Configure<AbpDefaultTenantStoreOptions>(options =>
  14. {
  15. options.Tenants = new[]
  16. {
  17. new TenantConfiguration(
  18. Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id
  19. "tenant1" //Name
  20. ),
  21. new TenantConfiguration(
  22. Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id
  23. "tenant2" //Name
  24. )
  25. {
  26. //tenant2 有单独的数据库连接字符串
  27. ConnectionStrings =
  28. {
  29. {ConnectionStrings.DefaultConnectionStringName, "..."}
  30. }
  31. }
  32. };
  33. });
  34. }
  35. }
  36. }
例子:appsettings.json定义租户

首先从appsetting.json文件中创建你的配置.

  1. using System.IO;
  2. using Microsoft.Extensions.Configuration;
  3. using Microsoft.Extensions.DependencyInjection;
  4. using Volo.Abp.Modularity;
  5. using Volo.Abp.MultiTenancy;
  6. namespace MyCompany.MyProject
  7. {
  8. [DependsOn(typeof(AbpMultiTenancyModule))]
  9. public class MyModule : AbpModule
  10. {
  11. public override void ConfigureServices(ServiceConfigurationContext context)
  12. {
  13. var configuration = BuildConfiguration();
  14. Configure<AbpDefaultTenantStoreOptions>(configuration);
  15. }
  16. private static IConfigurationRoot BuildConfiguration()
  17. {
  18. return new ConfigurationBuilder()
  19. .SetBasePath(Directory.GetCurrentDirectory())
  20. .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
  21. .Build();
  22. }
  23. }
  24. }

然后在appsettings.json中添加 “Tenants“ 节点:

  1. "Tenants": [
  2. {
  3. "Id": "446a5211-3d72-4339-9adc-845151f8ada0",
  4. "Name": "tenant1"
  5. },
  6. {
  7. "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
  8. "Name": "tenant2",
  9. "ConnectionStrings": {
  10. "Default": "...write tenant2's db connection string here..."
  11. }
  12. }
  13. ]
Volo.Abp… Package (TODO)

TODO: This package implements ITenantStore using a real database…

租户信息

ITenantStore跟 TenantConfiguration类一起工作,并且包含了几个租户属性:

  • Id:租户的唯一Id.
  • Name: 租户的唯一名称.
  • ConnectionStrings:如果这个租户有专门的数据库来存储数据.它可以提供数据库的字符串(它可以具有默认的连接字符串和每个模块的连接字符串).

多租户应用程序可能需要其他租户属性,但这些属性是框架与多个租户一起使用的最低要求.

代码中改变租户

TODO…

Volo.Abp.AspNetCore.MultiTenancy

Volo.Abp.AspNetCore.MultiTenancy将多租户整合到了ASP.NET Core的程序中.在PMC中使用下面的代码将它安装到项目中.

  1. Install-Package Volo.Abp.AspNetCore.MultiTenancy

然后添加 AbpAspNetCoreMultiTenancyModule 依赖到你的模块:

  1. using Volo.Abp.Modularity;
  2. using Volo.Abp.AspNetCore.MultiTenancy;
  3. namespace MyCompany.MyProject
  4. {
  5. [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))]
  6. public class MyModule : AbpModule
  7. {
  8. //...
  9. }
  10. }

多租户中间件

Volo.Abp.AspNetCore.MultiTenancy包含了多租户中间件…

  1. app.UseMultiTenancy();

TODO:…

从Web请求中确定当前租户

Volo.Abp.AspNetCore.MultiTenancy 添加了下面这些租户解析器,从当前Web请求(按优先级排序)中确定当前租户.

  • CurrentUserTenantResolveContributor: 如果当前用户已登录,从当前用户的声明中获取租户Id. 出于安全考虑,应该始终将其做为第一个Contributor.
  • QueryStringTenantResolveContributor: 尝试从query string参数中获取当前租户,默认参数名为”__tenant”.
  • FormTenantResolveContributor: 尝试从form参数中获取当前租户,默认参数名为”__tenant”.
  • RouteTenantResolveContributor:尝试从当前路由中获取(URL路径),默认是变量名是”__tenant”.所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户.
  • HeaderTenantResolveContributor: 尝试从HTTP header中获取当前租户,默认的header名称是”__tenant”.
  • CookieTenantResolveContributor: 尝试从当前cookie中获取当前租户.默认的Cookie名称是”__tenant”.

如果你使用nginx作为反向代理服务器,请注意如果TenantKey包含下划线或其他特殊字符可能存在问题, 请参考: http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers

可以使用AbpAspNetCoreMultiTenancyOptions修改默认的参数名”__tenant”.例如:

  1. services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
  2. {
  3. options.TenantKey = "MyTenantKey";
  4. });
域名租户解析器

实际项目中,大多数情况下你想通过子域名(如mytenant1.mydomain.com)或全域名(如mytenant.com)中确定当前租户.如果是这样,你可以配置AbpTenantResolveOptions添加一个域名租户解析器.

例子:添加子域名解析器
  1. using Microsoft.Extensions.DependencyInjection;
  2. using Volo.Abp.AspNetCore.MultiTenancy;
  3. using Volo.Abp.Modularity;
  4. using Volo.Abp.MultiTenancy;
  5. namespace MyCompany.MyProject
  6. {
  7. [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))]
  8. public class MyModule : AbpModule
  9. {
  10. public override void ConfigureServices(ServiceConfigurationContext context)
  11. {
  12. Configure<AbpTenantResolveOptions>(options =>
  13. {
  14. //子域名格式: {0}.mydomain.com (作为第二优先级解析器添加, 位于CurrentUserTenantResolveContributor之后)
  15. options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));
  16. });
  17. //...
  18. }
  19. }
  20. }

{0}是用来确定当前租户唯一名称的占位符.

你可以使用下面的方法,代替options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));:

  1. options.AddDomainTenantResolver("{0}.mydomain.com");
例子:添加全域名解析器
  1. options.AddDomainTenantResolver("{0}.com");