多租户抽象

租户对象

  • ICurrentTenant : 当前租户
  • ICurrentTenantAccessor : 当前租户访问器
  • BasicTenantInfo : 租户实际对象

租户存储

ITenantStore是多租户抽象数据源,可以实现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
{
// 对应配置文件Tenants节点
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", // 租户的唯一Id
"Name": "tenant2", // 租户的唯一名称
"ConnectionStrings": { // 如果这个租户有专门的数据库来存储数据.它可以提供数据库的字符串(它可以具有默认的连接字符串和每个模块的连接字符串).
"Default": "...write tenant2's db connection string here..."
}
}
]
}

租户信息

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

  • 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; }

//TODO: We can make this async
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 = ... //从其他地方获取租户id或租户名字...
}))
});

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 =>
{
//子域名格式: {0}.mydomain.com (作为第二优先级解析器添加, 位于CurrentUserTenantResolveContributor之后)
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); //TODO: A domain exception would be better..?
}
}
}

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)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!
{
var tenant = await TenantRepository.FindByNameAsync(name);
if (tenant == null)
{
return null;
}

return ObjectMapper.Map<Tenant, TenantConfiguration>(tenant);
}
}
// 后续代码省略
}