ABP Framework-MultiTenancy源码解析
目录
https://abp.io/docs/6.0/Multi-Tenancy
Version
6.0.3
Package
Volo.Abp.AspNetCore.MultiTenancy
Volo.Abp.MultiTenancy
//独立模块
Volo.Abp.TenantManagement.*
多租户中间件
如想要使用上多租户,中间件是必不可少的一部分,MultiTenancyMiddleware.cs。
请求进入后,首先进行的是租户解析,简要查看租户中间件源码,省略与简化后如下:
public class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 从请求信息中解析拿到租户信息
var tenant = await _tenantConfigurationProvider.GetAsync(saveResolveResult: true);
// 如果是新租户则切换上下文
if (tenant?.Id != _currentTenant.Id)
{
// 更新当前租户
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
if (_tenantResolveResultAccessor.Result != null &&
_tenantResolveResultAccessor.Result.AppliedResolvers.Contains(QueryStringTenantResolveContributor.ContributorName))
{
// 租户信息记录到Cookie中
AbpMultiTenancyCookieHelper.SetTenantCookie(context, _currentTenant.Id, _options.TenantKey);
}
// 下一个中间件
await next(context);
}
}
else
{
// 下一个中间件
await next(context);
}
}
}
如上核心部分几个抽象接口:
ITenantConfigurationProvider: 解析请求中的租户信息。
ITenantResolver: 编排循环解析租户信息。
ITenantResolveResultAccessor: 保存解析后的结果,解析成功与否都保存。
ICurrentTenant: 存储当前请求的租户信息以便于后续代码中使用。
ITenantStore: 根据Id或Name从配置或库或远程服务中获取详细信息。
核心解析
ITenantConfigurationProvider
作为多租户中间件的核心调用,该部分代码简要查看
public class TenantConfigurationProvider: ITenantConfigurationProvider, ITransientDependency
{
public virtual async Task<TenantConfiguration> GetAsync(bool saveResolveResult = false)
{
// 从请求信息中得到解析结果
var resolveResult = await TenantResolver.ResolveTenantIdOrNameAsync();
if (saveResolveResult)
{
// 保存解析结果
TenantResolveResultAccessor.Result = resolveResult;
}
TenantConfiguration tenant = null;
if (resolveResult.TenantIdOrName != null)
{
//从Store中解析得到详细租户信息
tenant = await TenantStore.FindAsync(resolveResult.TenantIdOrName);
if (tenant == null || !tenant.IsActive)
{
// throw new Exception();
}
}
return tenant;
}
}
代码中的直接依赖如下
ITenantResolver 从Cookie/Header/QueryString/Route/Form/CurrentUser等解析出Tenant。
AbpTenantResolveOptions:保存多种解析方式,具体属性是TenantResolvers,其类型为ITenantResolveContributor,从各种途径解析获取到TenantId。
QueryStringTenantResolveContributor
FormTenantResolveContributor
RouteTenantResolveContributor
HeaderTenantResolveContributor
CookieTenantResolveContributor
DomainTenantResolveContributor
ActionTenantResolveContributor
CurrentUserTenantResolveContributor
CustomTenantResolveContributor
TenantStore:根据TenantId从Store中查询出Tenant信息。
ITenantResolveResultAccessor:存储Tenant结果。
ITenantResolver
在这其中注入Options,随后并循环调用提前配置在Options中的ITenantResolveContributor。
如下代码简化TenantResolver的具体实现:
public class TenantResolver : ITenantResolver, ITransientDependency
{
public virtual async Task<TenantResolveResult> ResolveTenantIdOrNameAsync()
{
var result = new TenantResolveResult();
using (var serviceScope = _serviceProvider.CreateScope())
{
var context = new TenantResolveContext(serviceScope.ServiceProvider);
// 循环配置好的TenantResolvers
foreach (var tenantResolver in _options.TenantResolvers)
{
// 进入到具体的TenantResolveContributor中解析并将结果记录到context中
await tenantResolver.ResolveAsync(context);
//记录循环中的每一个TenantResolverContributor
result.AppliedResolvers.Add(tenantResolver.Name);
if (context.HasResolvedTenantOrHost())
{
result.TenantIdOrName = context.TenantIdOrName;
break;
}
}
}
return result;
}
}
第15行会将结果保存到ITenantResolveContext,该部分实现类图如下
ITenantResolveResultAccessor
当经过TenantResolver解析拿到了具体的Tenant后,将解析结果TenantResolveResult存入到TenantResolveResultAccessor中,以便于后续使用。
// 从请求信息中得到解析结果
var resolveResult = await TenantResolver.ResolveTenantIdOrNameAsync();
if (saveResolveResult)
{
// 保存解析结果
TenantResolveResultAccessor.Result = resolveResult;
}
在默认实现HttpContextTenantResolveResultAccessor中保存在HttpContext,这个实现位于Volo.Abp.AspNetCore.MultiTenancy,利用了HttpContext.Items作为承载整个请求内都可以使用。
[Dependency(ReplaceServices = true)]
public class HttpContextTenantResolveResultAccessor : ITenantResolveResultAccessor, ITransientDependency
{
public const string HttpContextItemName = "__AbpTenantResolveResult";
public TenantResolveResult Result {
get => _httpContextAccessor.HttpContext?.Items[HttpContextItemName] as TenantResolveResult;
set {
if (_httpContextAccessor.HttpContext == null)
{
return;
}
_httpContextAccessor.HttpContext.Items[HttpContextItemName] = value;
}
}
}
如果想要在其他模式中,比如控制台中也想要类似功能,可以参照HttpContextTenantResolveResultAccessor改写,总归有了ITenantResolveResultAccessor扩展性更好了。
多租户中间件中当识别到租户并更换到新的租户时,如果在TenantResolveResult的AppliedResolvers记录中有存在QueryStringTenantResolveContributor,也就是全局配置时候没有移除这个(默认配置是有这个),则会设置租户信息到Cookie中。
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
if (_tenantResolveResultAccessor.Result != null &&
_tenantResolveResultAccessor.Result.AppliedResolvers.Contains(QueryStringTenantResolveContributor.ContributorName))
{
AbpMultiTenancyCookieHelper.SetTenantCookie(context, _currentTenant.Id, _options.TenantKey);
}
}
ICurrentTenant
在多租户中间件中,会将有效租户设置到CurrentTenant中,
public class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// 从请求信息中解析拿到租户信息
var tenant = await _tenantConfigurationProvider.GetAsync(saveResolveResult: true);
// 如果是新租户则切换上下文
if (tenant?.Id != _currentTenant.Id)
{
// 更新当前租户
using (_currentTenant.Change(tenant?.Id, tenant?.Name))
{
// 下一个中间件
await next(context);
}
}
else
{
// 下一个中间件
await next(context);
}
}
}
CurrentTenant的设计如下,尽管Id和Name处在CurrentTenant实现中,但是其具体值又是存储在ICurrentTenantAccessor中。
对于WebAssemblyCurrentTenantAccessor其实际使用场景是Blazor Wasm模式下。对于后端请求来讲,只关注AsyncLocalCurrentTenantAccessor即可。在这其中最终租户信息保存在CurrentScope中。
public class AsyncLocalCurrentTenantAccessor : ICurrentTenantAccessor
{
public static AsyncLocalCurrentTenantAccessor Instance { get; } = new();
public BasicTenantInfo Current {
get => _currentScope.Value;
set => _currentScope.Value = value;
}
private readonly AsyncLocal<BasicTenantInfo> _currentScope;
private AsyncLocalCurrentTenantAccessor()
{
_currentScope = new AsyncLocal<BasicTenantInfo>();
}
}
ITenantStore
在从请求信息中拿到TenantIdOrName后,会调用ITenantStore来验证租户是否存在,
如果不考虑Volo.Abp.TenantManagment模块,MvcRemoteTenantStore是采用远程请求多租户服务的方式,属于前端独立项目中。
默认的DefaultTenantStore会从配置文件appsettings.json中读取多租户基础数据,根据Id Or Name补充其他信息。
[Dependency(TryRegister = true)]
public class DefaultTenantStore : ITenantStore, ITransientDependency
{
private readonly AbpDefaultTenantStoreOptions _options;
public DefaultTenantStore(IOptionsMonitor<AbpDefaultTenantStoreOptions> options)
{
_options = options.CurrentValue;
}
public Task<TenantConfiguration> FindAsync(string name)
{
return Task.FromResult(Find(name));
}
public Task<TenantConfiguration> FindAsync(Guid id)
{
return Task.FromResult(Find(id));
}
}
而在TenantManagement模块中,则是读取数据库并缓存。
public class TenantStore : ITenantStore, ITransientDependency
{
protected ITenantRepository TenantRepository { get; }
protected IObjectMapper<AbpTenantManagementDomainModule> ObjectMapper { get; }
protected ICurrentTenant CurrentTenant { get; }
protected IDistributedCache<TenantCacheItem> Cache { get; }
public TenantStore(
ITenantRepository tenantRepository,
IObjectMapper<AbpTenantManagementDomainModule> objectMapper,
ICurrentTenant currentTenant,
IDistributedCache<TenantCacheItem> cache)
{
TenantRepository = tenantRepository;
ObjectMapper = objectMapper;
CurrentTenant = currentTenant;
Cache = cache;
}
public virtual async Task<TenantConfiguration> FindAsync(string name)
{
return (await GetCacheItemAsync(null, name)).Value;
}
public virtual async Task<TenantConfiguration> FindAsync(Guid id)
{
return (await GetCacheItemAsync(id, null)).Value;
}
protected virtual async Task<TenantCacheItem> GetCacheItemAsync(Guid? id, string name)
{
var cacheKey = CalculateCacheKey(id, name);
var cacheItem = await Cache.GetAsync(cacheKey, considerUow: true);
if (cacheItem != null)
{
return cacheItem;
}
if (id.HasValue)
{
using (CurrentTenant.Change(null)) //TODO: No need this if we can implement to define host side (or tenant-independent) entities!
{
var tenant = await TenantRepository.FindAsync(id.Value);
return await SetCacheAsync(cacheKey, tenant);
}
}
if (!name.IsNullOrWhiteSpace())
{
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);
return await SetCacheAsync(cacheKey, tenant);
}
}
throw new AbpException("Both id and name can't be invalid.");
}
}
TenantManagement模块中,更多的是对租户的增删改查及对连接字符串的管理。可不需要细看。
扩展
如果想要根据其他方式来推断多租户,比如想要租户内用户Email唯一并且登录时候使用Email登录,但是不选择租户,可以扩展TenantResolveContributor,从HttpContext中找到Form中的UserEmail,再查询用户信息,拿到TenantId,这样不用再页面上选择或者根据域名方式区分租户。
如果不想要在请求返回时候还有Cookie信息返回,可以在TenantResolve配置中移除QueryStringTenantResolveContributor,甚至是为了极致性能,确定只有一种租户或只有确定的几种租户选择方式时,都可以挪去不必要的解析来源。
public class AbpAspNetCoreMultiTenancyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpTenantResolveOptions>(options =>
{
options.TenantResolvers.Add(new QueryStringTenantResolveContributor());
options.TenantResolvers.Add(new FormTenantResolveContributor());
options.TenantResolvers.Add(new RouteTenantResolveContributor());
options.TenantResolvers.Add(new HeaderTenantResolveContributor());
options.TenantResolvers.Add(new CookieTenantResolveContributor());
});
}
}
- 在集成测试中,如果想要集成测试支持多租户,因为不会直接请求Asp.Net Core(集成测试直接对ApplicationService调用),因此多租户中间件不生效,也就使得不能切换租户。可以在集成测试开始测试前更改,调用服务设置。如此一来,当需要多租户集成测试场景时,也能够进行支持了。
var currentTenantAccessor = serviceProvider.GetRequiredService<ICurrentTenantAccessor>();
currentTenantAccessor.Current = new BasicTenantInfo(defaultTenantOptions.Id, defaultTenantOptions.Name);
2024-05-19,望技术有成后能回来看见自己的脚步。