多租户抽象 租户对象
ICurrentTenant
: 当前租户
ICurrentTenantAccessor
: 当前租户访问器
BasicTenantInfo
: 租户实际对象
租户存储 ITenantStore
是多租户抽象数据源,可以实现ITenantStore,存储租户数据源
存储接口
1 2 3 4 5 6 7 8 9 10 public interface ITenantStore { Task<TenantConfiguration> FindAsync (string name ) ; Task<TenantConfiguration> FindAsync (Guid id ) ; TenantConfiguration Find (string name ) ; TenantConfiguration Find (Guid id ) ; }
配置数据存储 默认从配置文件中读取信息作为多租户存储
DefaultTenantStore
: 从配置文件中读取多租户信息
配置对应选项类
1 2 3 4 5 6 7 8 9 10 public class AbpDefaultTenantStoreOptions { public TenantConfiguration[] Tenants { get ; set ; } public AbpDefaultTenantStoreOptions () { Tenants = new TenantConfiguration[0 ]; } }
配置文件示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "Tenants" : [ { "Id" : "446a5211-3d72-4339-9adc-845151f8ada0" , "Name" : "tenant1" } , { "Id" : "25388015-ef1c-4355-9c18-f6b6ddbaf89d" , "Name" : "tenant2" , "ConnectionStrings" : { "Default" : "...write tenant2's db connection string here..." } } ] }
租户信息 ITenantStore
跟TenantConfiguration
类一起工作,并且包含了几个租户属性:
Tenant
: 多租户领域聚合根
TenantConnectionString
: 多租户连接字符串实体
Id
: 租户的唯一Id.
Name
: 租户的唯一名称.
ConnectionStrings
: 如果这个租户有专门的数据库来存储数据.它可以提供数据库的字符串(它可以具有默认的连接字符串和每个模块的连接字符串).
多租户应用程序可能需要其他租户属性,但这些属性是框架与多个租户一起使用的最低要求.
租户解析 抽象定义
ITenantResolver
: 租户解析器
ITenantResolveContext
: 租户解析上下文
ITenantResolveContributor
: 租户解析提供者
ITenantResolveResultAccessor
: 租户解析结果访问器
TenantResolveResult
: 租户解析结果
租户解析器
默认解析器
TenantResolveContributorBase
: 解析提供者抽象类
1 2 3 4 5 6 7 public abstract class TenantResolveContributorBase : ITenantResolveContributor { public abstract string Name { get ; } public abstract void Resolve (ITenantResolveContext context ) ; }
CurrentUserTenantResolveContributor
: 当前用户解析器
ActionTenantResolveContributor
: 委托解析器,方便开发人员直接在AbpTenantResolveOptions添加设置租户信息逻辑
示例代码
1 2 3 4 5 6 7 Configure<AbpTenantResolveOptions>(options => { options.TenantResolvers.Add(new ActionTenantResolveContributor(context => { context.TenantIdOrName = ... })) });
Volo.Abp.AspNetCore.MultiTenancy
包内实现了多种租户解析器,从当前Web请求(从子域名,请求头,cookie,路由…等
多租户Web实现 多租户中间件
MultiTenancyMiddleware
: 多租户中间件
多租户中间件
负责解析租户信息,依次从解析方法提供者中解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public async Task InvokeAsync (HttpContext context, RequestDelegate next ){ var resolveResult = _tenantResolver.ResolveTenantIdOrName(); _tenantResolveResultAccessor.Result = resolveResult; TenantConfiguration tenant = null ; if (resolveResult.TenantIdOrName != null ) { tenant = await FindTenantAsync(resolveResult.TenantIdOrName); if (tenant == null ) { throw new BusinessException( code: "Volo.AbpIo.MultiTenancy:010001" , message: "Tenant not found!" , details: "There is no tenant with the tenant id or name: " + resolveResult.TenantIdOrName ); } } using (_currentTenant.Change(tenant?.Id, tenant?.Name)) { await next(context); } }
租户解析器
HttpTenantResolveContributorBase
: Web租户解析器抽象类
Volo.Abp.AspNetCore.MultiTenancy
实现了多种租户解析器,从当前Web请求(按优先级排序)中确定当前租户.
CurrentUserTenantResolveContributor
: 当前用户解析器(基础模块中实现),如果当前用户已登录,从当前用户的声明中获取租户Id. 出于安全考虑,应该始终将其做为第一个Contributor .
QueryTenantResolveContributor
: 查询租户解析器,尝试从query string参数中获取当前租户,默认参数名为”__tenant”.
RouteTenantResolveContributor
: 路由租户解析器,尝试从当前路由中获取(URL路径),默认是变量名是”__tenant”.所以,如果你的路由中定义了这个变量,就可以从路由中确定当前租户.
HeaderTenantResolveContributor
: 请求头租户解析器,尝试从HTTP header中获取当前租户,默认的header名称是”__tenant”.
CookieTenantResolveContributor
: Cookie租户解析器,通过Cookie中取当前租户值,默认的Cookie名称是”__tenant”.
可以通过配置文件类AbpAspNetCoreMultiTenancyOptions
来更改参数名”__tenant”
1 2 3 4 services.Configure<AbpAspNetCoreMultiTenancyOptions>(options => { options.TenantKey = "MyTenantKey" ; });
域名租户解析器 实际项目中,大多数情况下你想通过子域名(如mytenant1.mydomain.com)或全域名(如mytenant.com)中确定当前租户.如果是这样,你可以配置AbpTenantResolveOptions添加一个域名租户解析器.
DomainTenantResolveContributor
: 域名租户解析器
添加子域名解析器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using Microsoft.Extensions.DependencyInjection;using Volo.Abp.AspNetCore.MultiTenancy;using Volo.Abp.Modularity;using Volo.Abp.MultiTenancy;namespace MyCompany.MyProject { [DependsOn(typeof(AbpAspNetCoreMultiTenancyModule)) ] public class MyModule : AbpModule { public override void ConfigureServices (ServiceConfigurationContext context ) { Configure<AbpTenantResolveOptions>(options => { options.TenantResolvers.Insert(1 , new DomainTenantResolveContributor("{0}.mydomain.com" )); }); } } }
多租户基础模块里默认添加CurrentUserTenantResolveContributor,如果使用Volo.Abp.AspNetCore.MultiTenancy
模块包,则会添加上述几个解析器。
根据不同的需求可以在实际使用切换不同的解析器
多租户模块实现 Abp框架提供了多个模块,TenantManagement就是其中之一,该模块提供了多租户的创建,存储,修改等功能的实现
多租户存储数据库持久化
TenantManager
: 多租户的领域服务,提供创建和修改业务
ITenantRepository
: 多租户的领域仓储,提供持久化实现
TenantStore
: 多租户存储实现
TenantManager
将涉及到仓储对象和领域对象的复杂业务放在领域服务中,再通过应用服务暴露给其他模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class TenantManager : DomainService , ITenantManager { protected ITenantRepository TenantRepository { get ; } public TenantManager (ITenantRepository tenantRepository ) { TenantRepository = tenantRepository; } public virtual async Task<Tenant> CreateAsync (string name ) { Check.NotNull(name, nameof (name)); await ValidateNameAsync(name); return new Tenant(GuidGenerator.Create(), name); } public virtual async Task ChangeNameAsync (Tenant tenant, string name ) { Check.NotNull(tenant, nameof (tenant)); Check.NotNull(name, nameof (name)); await ValidateNameAsync(name, tenant.Id); tenant.SetName(name); } protected virtual async Task ValidateNameAsync (string name, Guid? expectedId = null ) { var tenant = await TenantRepository.FindByNameAsync(name); if (tenant != null && tenant.Id != expectedId) { throw new UserFriendlyException("Duplicate tenancy name: " + name); } } }
TenantStore
基于数据库实现ITenantStore
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class TenantStore : ITenantStore , ITransientDependency { protected ITenantRepository TenantRepository { get ; } protected IObjectMapper<AbpTenantManagementDomainModule> ObjectMapper { get ; } protected ICurrentTenant CurrentTenant { get ; } public TenantStore ( ITenantRepository tenantRepository, IObjectMapper<AbpTenantManagementDomainModule> objectMapper, ICurrentTenant currentTenant ) { TenantRepository = tenantRepository; ObjectMapper = objectMapper; CurrentTenant = currentTenant; } public virtual async Task<TenantConfiguration> FindAsync (string name ) { using (CurrentTenant.Change(null )) { var tenant = await TenantRepository.FindByNameAsync(name); if (tenant == null ) { return null ; } return ObjectMapper.Map<Tenant, TenantConfiguration>(tenant); } } }