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结果。

204513110_0c3a37d7-7497-4fa9-99f5-ddb51436baeb

ITenantResolver

在这其中注入Options,随后并循环调用提前配置在Options中的ITenantResolveContributor。

204515241_c60b6f84-af31-42c5-b63e-ca93b14e75e3 如下代码简化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,该部分实现类图如下 204516371_9fb97c65-3fd4-4062-909b-61b6ce5ec5b4

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扩展性更好了。 204517425_ba0357a2-827d-475f-80ee-9cfbcf46d8bd 多租户中间件中当识别到租户并更换到新的租户时,如果在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中。 204518529_19b5dd64-5204-44d8-ab54-ca86f66be026 对于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来验证租户是否存在,

204519584_94527ddc-0b5e-475e-9a86-de83d429404f 如果不考虑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,望技术有成后能回来看见自己的脚步。