天天看點

[Abp vNext 源碼分析] - 19. 多租戶

一、簡介

ABP vNext 原生支援多租戶體系,可以讓開發人員快速地基于架構開發 SaaS 系統。ABP vNext 實作多租戶的思路也非常簡單,通過一個

TenantId

來分割各個租戶的資料,并且在查詢的時候使用統一的全局過濾器(類似于軟删除)來篩選資料。

關于多租戶體系的東西,基本定義與核心邏輯存放在 Volo.ABP.MultiTenancy 内部。針對 ASP.NET Core MVC 的內建則是由 Volo.ABP.AspNetCore.MultiTenancy 項目實作的,針對多租戶的解析都在這個項目内部。租戶資料的存儲和管理都由 Volo.ABP.TenantManagement 子產品提供,開發人員也可以直接使用該項目快速實作多租戶功能。

二、源碼分析

2.1 啟動子產品

AbpMultiTenancyModule

子產品是啟用整個多租戶功能的核心子產品,内部隻進行了一個動作,就是從配置類當中讀取多租戶的基本資訊,以 JSON Provider 為例,就需要在

appsettings.json

裡面有

Tenants

節。

"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..."
      }
    }
  ]
           

2.1.1 預設租戶來源

這裡的資料将會作為預設租戶來源,也就是說在确認目前租戶的時候,會從這裡面的資料與要登入的租戶進行比較,如果不存在則不允許進行操作。

public interface ITenantStore
{
    Task<TenantConfiguration> FindAsync(string name);

    Task<TenantConfiguration> FindAsync(Guid id);

    TenantConfiguration Find(string name);

    TenantConfiguration Find(Guid id);
}
           

預設的存儲實作:

[Dependency(TryRegister = true)]
public class DefaultTenantStore : ITenantStore, ITransientDependency
{
    // 直接從 Options 當中擷取租戶資料。
    private readonly AbpDefaultTenantStoreOptions _options;

    public DefaultTenantStore(IOptionsSnapshot<AbpDefaultTenantStoreOptions> options)
    {
        _options = options.Value;
    }

    public Task<TenantConfiguration> FindAsync(string name)
    {
        return Task.FromResult(Find(name));
    }

    public Task<TenantConfiguration> FindAsync(Guid id)
    {
        return Task.FromResult(Find(id));
    }

    public TenantConfiguration Find(string name)
    {
        return _options.Tenants?.FirstOrDefault(t => t.Name == name);
    }

    public TenantConfiguration Find(Guid id)
    {
        return _options.Tenants?.FirstOrDefault(t => t.Id == id);
    }
}
           

除了從配置檔案當中讀取租戶資訊以外,開發人員也可以自己實作

ITenantStore

接口,比如說像 TenantManagement 一樣,将租戶資訊存儲到資料庫當中。

2.1.2 基于資料庫的租戶存儲

話接上文,我們說過在 Volo.ABP.TenantManagement 子產品内部有提供另一種

ITenantStore

接口的實作,這個類型叫做

TenantStore

,内部邏輯也很簡單,就是從倉儲當中查找租戶資料。

public class TenantStore : ITenantStore, ITransientDependency
{
    private readonly ITenantRepository _tenantRepository;
    private readonly IObjectMapper<AbpTenantManagementDomainModule> _objectMapper;
    private readonly ICurrentTenant _currentTenant;

    public TenantStore(
        ITenantRepository tenantRepository, 
        IObjectMapper<AbpTenantManagementDomainModule> objectMapper,
        ICurrentTenant currentTenant)
    {
        _tenantRepository = tenantRepository;
        _objectMapper = objectMapper;
        _currentTenant = currentTenant;
    }

    public 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);
        }
    }

    // ... 其他的代碼已經省略。
}
           

可以看到,最後也是傳回的一個

TenantConfiguration

類型。關于這個類型,是 ABP 在多租戶核心庫定義的一個基本類型之一,主要是用于規定持久化一個租戶資訊需要包含的屬性。

[Serializable]
public class TenantConfiguration
{
    // 租戶的 Guid。
    public Guid Id { get; set; }

    // 租戶的名稱。
    public string Name { get; set; }

    // 租戶對應的資料庫連接配接字元串。
    public ConnectionStrings ConnectionStrings { get; set; }

    public TenantConfiguration()
    {
        
    }

    public TenantConfiguration(Guid id, [NotNull] string name)
    {
        Check.NotNull(name, nameof(name));

        Id = id;
        Name = name;

        ConnectionStrings = new ConnectionStrings();
    }
}
           

2.2 租戶的解析

ABP vNext 如果要判斷目前的租戶是誰,則是通過

AbpTenantResolveOptions

提供的一組

ITenantResolveContributor

進行處理的。

public class AbpTenantResolveOptions
{
    // 會使用到的這組解析對象。
    [NotNull]
    public List<ITenantResolveContributor> TenantResolvers { get; }

    public AbpTenantResolveOptions()
    {
        TenantResolvers = new List<ITenantResolveContributor>
        {
            // 預設的解析對象,會通過 Token 内字段解析目前租戶。
            new CurrentUserTenantResolveContributor()
        };
    }
}
           

這裡的設計與權限一樣,都是由一組 解析對象(解析器) 進行處理,在上層開放的入口隻有一個

ITenantResolver

,内部通過

foreach

執行這組解析對象的

Resolve()

方法。

下面就是我們

ITenantResolver

的預設實作

TenantResolver

,你可以在任何時候調用它。比如說你在想要獲得目前租戶 Id 的時候。不過一般不推薦這樣做,因為 ABP 已經給我們提供了

MultiTenancyMiddleware

中間件。

[Abp vNext 源碼分析] - 19. 多租戶

也就是說,在每次請求的時候,都會将這個

Id

通過

ICurrentTenant.Change()

進行變更,那麼在這個請求執行完成之前,通過

ICurrentTenant

取得的

Id

都會是解析器解析出來的 Id。

public class TenantResolver : ITenantResolver, ITransientDependency
{
    private readonly IServiceProvider _serviceProvider;
    private readonly AbpTenantResolveOptions _options;

    public TenantResolver(IOptions<AbpTenantResolveOptions> options, IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
        _options = options.Value;
    }

    public TenantResolveResult ResolveTenantIdOrName()
    {
        var result = new TenantResolveResult();

        using (var serviceScope = _serviceProvider.CreateScope())
        {
            // 建立一個解析上下文,用于存儲解析器的租戶 Id 解析結果。
            var context = new TenantResolveContext(serviceScope.ServiceProvider);

            // 周遊執行解析器。
            foreach (var tenantResolver in _options.TenantResolvers)
            {
                tenantResolver.Resolve(context);

                result.AppliedResolvers.Add(tenantResolver.Name);

                // 如果有某個解析器為上下文設定了值,則跳出。
                if (context.HasResolvedTenantOrHost())
                {
                    result.TenantIdOrName = context.TenantIdOrName;
                    break;
                }
            }
        }

        return result;
    }
}
           

2.2.1 預設的解析對象

如果不使用 Volo.Abp.AspNetCore.MultiTenancy 子產品,ABP vNext 會調用

CurrentUserTenantResolveContributor

解析目前操作的租戶。

public class CurrentUserTenantResolveContributor : TenantResolveContributorBase
{
    public const string ContributorName = "CurrentUser";

    public override string Name => ContributorName;

    public override void Resolve(ITenantResolveContext context)
    {
        // 從 Token 當中擷取目前登入使用者的資訊。
        var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
        if (currentUser.IsAuthenticated != true)
        {
            return;
        }

        // 設定解析上下文,确認目前的租戶 Id。
        context.Handled = true;
        context.TenantIdOrName = currentUser.TenantId?.ToString();
    }
}
           

在這裡可以看到,如果從 Token 當中解析到了租戶 Id,會将這個 Id 傳遞給 解析上下文。這個上下文在最開始已經遇到過了,如果 ABP vNext 在解析的時候發現租戶 Id 被确認了,就不會執行剩下的解析器。

2.2.2 ABP 提供的其他解析器

ABP 在 Volo.Abp.AspNetCore.MultiTenancy 子產品當中還提供了其他幾種解析器,他們的作用分别如下。

解析器類型 作用 優先級

QueryStringTenantResolveContributor

通過 Query String 的

__tenant

參數确認租戶。
2

RouteTenantResolveContributor

通過路由判斷目前租戶。 3

HeaderTenantResolveContributor

通過 Header 裡面的

__tenant

确認租戶。
4

CookieTenantResolveContributor

通過攜帶的 Cookie 确認租戶。 5

DomainTenantResolveContributor

二級域名解析器,通過二級域名确定租戶。 第二

2.2.3 域名解析器

這裡比較有意思的是

DomainTenantResolveContributor

,開發人員可以通過

AbpTenantResolveOptions.AddDomainTenantResolver()

方法添加這個解析器。 域名解析器會通過解析二級域名來比對對應的租戶,例如我針對租戶 A 配置設定了一個二級域名

http://a.system.com

,那麼這個 a 就會被作為租戶名稱解析出來,最後傳遞給

ITenantResolver

解析器作為結果。

[Abp vNext 源碼分析] - 19. 多租戶

注意:

在使用 Header 作為租戶資訊提供者的時候,開發人員使用的是 NGINX 作為反向代理伺服器 時,需要在對應的 config 檔案内部配置

underscores_in_headers on;

選項。否則 ABP 所需要的

__tenantId

将會被過濾掉,或者你可以指定一個沒有下劃線的 Key。

域名解析器的詳細代碼解釋:

public class DomainTenantResolveContributor : HttpTenantResolveContributorBase
{
    public const string ContributorName = "Domain";

    public override string Name => ContributorName;

    private static readonly string[] ProtocolPrefixes = { "http://", "https://" };

    private readonly string _domainFormat;

    // 使用指定的格式來确定租戶字首,例如 “{0}.abp.io”。
    public DomainTenantResolveContributor(string domainFormat)
    {
        _domainFormat = domainFormat.RemovePreFix(ProtocolPrefixes);
    }

    protected override string GetTenantIdOrNameFromHttpContextOrNull(
        ITenantResolveContext context, 
        HttpContext httpContext)
    {
        // 如果 Host 值為空,則不進行任何操作。
        if (httpContext.Request?.Host == null)
        {
            return null;
        }

        // 解析具體的域名資訊,并進行比對。
        var hostName = httpContext.Request.Host.Host.RemovePreFix(ProtocolPrefixes);
        // 這裡的 FormattedStringValueExtracter 類型是 ABP 自己實作的一個格式化解析器。
        var extractResult = FormattedStringValueExtracter.Extract(hostName, _domainFormat, ignoreCase: true);

        context.Handled = true;

        if (!extractResult.IsMatch)
        {
            return null;
        }

        return extractResult.Matches[0].Value;
    }
}
           

從上述代碼可以知道,域名解析器是基于

HttpTenantResolveContributorBase

基類進行處理的,這個抽象基類會取得目前請求的一個

HttpContext

,将這個傳遞與解析上下文一起傳遞給子類實作,由子類實作負責具體的解析邏輯。

public abstract class HttpTenantResolveContributorBase : TenantResolveContributorBase
{
    public override void Resolve(ITenantResolveContext context)
    {
        // 擷取目前請求的上下文。
        var httpContext = context.GetHttpContext();
        if (httpContext == null)
        {
            return;
        }

        try
        {
            ResolveFromHttpContext(context, httpContext);
        }
        catch (Exception e)
        {
            context.ServiceProvider
                .GetRequiredService<ILogger<HttpTenantResolveContributorBase>>()
                .LogWarning(e.ToString());
        }
    }

    protected virtual void ResolveFromHttpContext(ITenantResolveContext context, HttpContext httpContext)
    {
        // 調用抽象方法,擷取具體的租戶 Id 或名稱。
        var tenantIdOrName = GetTenantIdOrNameFromHttpContextOrNull(context, httpContext);
        if (!tenantIdOrName.IsNullOrEmpty())
        {
            // 獲得到租戶辨別之後,填充到解析上下文。
            context.TenantIdOrName = tenantIdOrName;
        }
    }

    protected abstract string GetTenantIdOrNameFromHttpContextOrNull([NotNull] ITenantResolveContext context, [NotNull] HttpContext httpContext);
}
           

2.3 租戶資訊的傳遞

租戶解析器通過一系列的解析對象,擷取到了租戶或租戶 Id 之後,會将這些資料給哪些對象呢?或者說,ABP 在什麼地方調用了 租戶解析器,答案就是 中間件。

在 Volo.ABP.AspNetCore.MultiTenancy 子產品的内部,提供了一個

MultiTenancyMiddleware

開發人員如果需要使用 ASP.NET Core 的多租戶相關功能,也可以引入該子產品。并且在子產品的

OnApplicationInitialization()

方法當中,使用

IApplicationBuilder.UseMultiTenancy()

進行啟用。

這裡在啟用的時候,需要注意中間件的順序和位置,不要放到最末尾進行處理。

public class MultiTenancyMiddleware : IMiddleware, ITransientDependency
{
    private readonly ITenantResolver _tenantResolver;
    private readonly ITenantStore _tenantStore;
    private readonly ICurrentTenant _currentTenant;
    private readonly ITenantResolveResultAccessor _tenantResolveResultAccessor;

    public MultiTenancyMiddleware(
        ITenantResolver tenantResolver, 
        ITenantStore tenantStore, 
        ICurrentTenant currentTenant, 
        ITenantResolveResultAccessor tenantResolveResultAccessor)
    {
        _tenantResolver = tenantResolver;
        _tenantStore = tenantStore;
        _currentTenant = currentTenant;
        _tenantResolveResultAccessor = tenantResolveResultAccessor;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        // 通過租戶解析器,擷取目前請求的租戶資訊。
        var resolveResult = _tenantResolver.ResolveTenantIdOrName();
        _tenantResolveResultAccessor.Result = resolveResult;

        TenantConfiguration tenant = null;
        // 如果目前請求是屬于租戶請求。
        if (resolveResult.TenantIdOrName != null)
        {
            // 查詢指定的租戶 Id 或名稱是否存在,不存在則抛出異常。
            tenant = await FindTenantAsync(resolveResult.TenantIdOrName);
            if (tenant == null)
            {
                //TODO: A better exception?
                throw new AbpException(
                    "There is no tenant with given tenant id or name: " + resolveResult.TenantIdOrName
                );
            }
        }

        // 在接下來的請求當中,将會通過 ICurrentTenant.Change() 方法變更目前租戶,直到
        // 請求結束。
        using (_currentTenant.Change(tenant?.Id, tenant?.Name))
        {
            await next(context);
        }
    }

    private async Task<TenantConfiguration> FindTenantAsync(string tenantIdOrName)
    {
        // 如果可以格式化為 Guid ,則說明是租戶 Id。
        if (Guid.TryParse(tenantIdOrName, out var parsedTenantId))
        {
            return await _tenantStore.FindAsync(parsedTenantId);
        }
        else
        {
            return await _tenantStore.FindAsync(tenantIdOrName);
        }
    }
}
           

在取得了租戶的辨別(Id 或名稱)之後,将會通過

ICurrentTenant.Change()

方法變更目前租戶的資訊,變更了當租戶資訊以後,在程式的其他任何地方使用

ICurrentTenant.Id

取得的資料都是租戶解析器解析出來的資料。

下面就是這個目前租戶的具體實作,可以看到這裡采用了一個 經典手法-嵌套。這個手法在工作單元和資料過濾器有見到過,結合

DisposeAction()

using

語句塊結束的時候把目前的租戶 Id 值設定為父級 Id。即在同一個語句當中,可以通過嵌套

using

語句塊來處理不同的租戶。

using(_currentTenant.Change("A"))
{
    Logger.LogInformation(_currentTenant.Id);
    using(_currentTenant.Change("B"))
    {
        Logger.LogInformation(_currentTenant.Id);
    }
}
           

具體的實作代碼,這裡的

ICurrentTenantAccessor

内部實作就是一個

AsyncLocal<BasicTenantInfo>

,用于在一個異步請求内部進行資料傳遞。

public class CurrentTenant : ICurrentTenant, ITransientDependency
{
    public virtual bool IsAvailable => Id.HasValue;

    public virtual Guid? Id => _currentTenantAccessor.Current?.TenantId;

    public string Name => _currentTenantAccessor.Current?.Name;

    private readonly ICurrentTenantAccessor _currentTenantAccessor;

    public CurrentTenant(ICurrentTenantAccessor currentTenantAccessor)
    {
        _currentTenantAccessor = currentTenantAccessor;
    }

    public IDisposable Change(Guid? id, string name = null)
    {
        return SetCurrent(id, name);
    }

    private IDisposable SetCurrent(Guid? tenantId, string name = null)
    {
        var parentScope = _currentTenantAccessor.Current;
        _currentTenantAccessor.Current = new BasicTenantInfo(tenantId, name);
        return new DisposeAction(() =>
        {
            _currentTenantAccessor.Current = parentScope;
        });
    }
}
           

這裡的

BasicTenantInfo

TenantConfiguraton

不同,前者僅用于在程式當中傳遞使用者的基本資訊,而後者是用于定于持久化的标準模型。

2.4 租戶的使用

2.4.1 資料庫過濾

租戶的核心作用就是隔離不同客戶的資料,關于過濾的基本邏輯則是存放在

AbpDbContext<TDbContext>

的。從下面的代碼可以看到,在使用的時候會從注入一個

ICurrentTenant

接口,這個接口可以獲得從租戶解析器裡面取得的租戶 Id 資訊。并且還有一個

IsMultiTenantFilterEnabled()

方法來判定目前 是否應用租戶過濾器。

public abstract class AbpDbContext<TDbContext> : DbContext, IEfCoreDbContext, ITransientDependency
    where TDbContext : DbContext
{
    protected virtual Guid? CurrentTenantId => CurrentTenant?.Id;

    protected virtual bool IsMultiTenantFilterEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;
        
    // ... 其他的代碼。
        
    public ICurrentTenant CurrentTenant { get; set; }

    // ... 其他的代碼。

    protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() where TEntity : class
    {
        // 定義一個 Lambda 表達式。
        Expression<Func<TEntity, bool>> expression = null;

        // 如果聚合根/實體實作了軟删除接口,則建構一個軟删除過濾器。
        if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
        {
            expression = e => !IsSoftDeleteFilterEnabled || !EF.Property<bool>(e, "IsDeleted");
        }

        // 如果聚合根/實體實作了多租戶接口,則建構一個多租戶過濾器。
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            // 篩選 TenantId 為 CurrentTenantId 的資料。
            Expression<Func<TEntity, bool>> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property<Guid>(e, "TenantId") == CurrentTenantId;
            expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter);
        }

        return expression;
    }

    // ... 其他的代碼。
}
           

2.4.2 種子資料建構

在 Volo.ABP.TenantManagement 子產品當中,如果使用者建立了一個租戶,ABP 不隻是在租戶表插入一條新資料而已。它還會設定種子資料的 構造上下文,并且執行所有的 種子資料建構者(

IDataSeedContributor

)。

[Authorize(TenantManagementPermissions.Tenants.Create)]
public virtual async Task<TenantDto> CreateAsync(TenantCreateDto input)
{
    var tenant = await TenantManager.CreateAsync(input.Name);
    await TenantRepository.InsertAsync(tenant);

    using (CurrentTenant.Change(tenant.Id, tenant.Name))
    {
        //TODO: Handle database creation?

        //TODO: Set admin email & password..?
        await DataSeeder.SeedAsync(tenant.Id);
    }
    
    return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}
           

這些建構者當中,就包括租戶的超級管理者(admin)和角色建構,以及針對超級管理者角色進行權限指派操作。

這裡需要注意第二點,如果開發人員沒有指定超級管理者使用者和密碼,那麼還是會使用預設密碼為租戶生成超級管理者,具體原因看如下代碼。

public class IdentityDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    private readonly IIdentityDataSeeder _identityDataSeeder;

    public IdentityDataSeedContributor(IIdentityDataSeeder identityDataSeeder)
    {
        _identityDataSeeder = identityDataSeeder;
    }

    public Task SeedAsync(DataSeedContext context)
    {
        return _identityDataSeeder.SeedAsync(
            context["AdminEmail"] as string ?? "[email protected]",
            context["AdminPassword"] as string ?? "1q2w3E*",
            context.TenantId
        );
    }
}
           

是以開發人員要實作為不同租戶 生成随機密碼,那麼就不能夠使用 TenantManagement 提供的建立方法,而是需要自己編寫一個應用服務進行處理。

2.4.3 權限的控制

如果開發人員使用了 ABP 提供的 Volo.Abp.PermissionManagement 子產品,就會看到在它的種子資料構造者當中會對權限進行判定。因為有一些 超級權限 是租主才能夠授予的,例如租戶的增加、删除、修改等,這些超級權限在定義的時候就需要說明是否是資料租主獨有的。

關于這點,可以參考租戶管理子產品在權限定義時,傳遞的

MultiTenancySides.Host

參數。

public class AbpTenantManagementPermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var tenantManagementGroup = context.AddGroup(TenantManagementPermissions.GroupName, L("Permission:TenantManagement"));

        var tenantsPermission = tenantManagementGroup.AddPermission(TenantManagementPermissions.Tenants.Default, L("Permission:TenantManagement"), multiTenancySide: MultiTenancySides.Host);
        tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Create, L("Permission:Create"), multiTenancySide: MultiTenancySides.Host);
        tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Update, L("Permission:Edit"), multiTenancySide: MultiTenancySides.Host);
        tenantsPermission.AddChild(TenantManagementPermissions.Tenants.Delete, L("Permission:Delete"), multiTenancySide: MultiTenancySides.Host);
        tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageFeatures, L("Permission:ManageFeatures"), multiTenancySide: MultiTenancySides.Host);
        tenantsPermission.AddChild(TenantManagementPermissions.Tenants.ManageConnectionStrings, L("Permission:ManageConnectionStrings"), multiTenancySide: MultiTenancySides.Host);
    }

    private static LocalizableString L(string name)
    {
        return LocalizableString.Create<AbpTenantManagementResource>(name);
    }
}
           

下面是權限種子資料構造者的代碼:

public class PermissionDataSeedContributor : IDataSeedContributor, ITransientDependency
{
    protected ICurrentTenant CurrentTenant { get; }

    protected IPermissionDefinitionManager PermissionDefinitionManager { get; }
    protected IPermissionDataSeeder PermissionDataSeeder { get; }

    public PermissionDataSeedContributor(
        IPermissionDefinitionManager permissionDefinitionManager,
        IPermissionDataSeeder permissionDataSeeder,
        ICurrentTenant currentTenant)
    {
        PermissionDefinitionManager = permissionDefinitionManager;
        PermissionDataSeeder = permissionDataSeeder;
        CurrentTenant = currentTenant;
    }

    public virtual Task SeedAsync(DataSeedContext context)
    {
        // 通過 GetMultiTenancySide() 方法判斷目前執行
        // 種子構造者的租戶情況,是租主還是租戶。
        var multiTenancySide = CurrentTenant.GetMultiTenancySide();
        // 根據條件篩選權限。
        var permissionNames = PermissionDefinitionManager
            .GetPermissions()
            .Where(p => p.MultiTenancySide.HasFlag(multiTenancySide))
            .Select(p => p.Name)
            .ToArray();

        // 将權限授予具體租戶的角色。
        return PermissionDataSeeder.SeedAsync(
            RolePermissionValueProvider.ProviderName,
            "admin",
            permissionNames,
            context.TenantId
        );
    }
}
           

而 ABP 在判斷目前是租主還是租戶的方法也很簡單,如果目前租戶 Id 為 NULL 則說明是租主,如果不為空則說明是具體租戶。

public static MultiTenancySides GetMultiTenancySide(this ICurrentTenant currentTenant)
{
    return currentTenant.Id.HasValue
        ? MultiTenancySides.Tenant
        : MultiTenancySides.Host;
}
           

2.4.4 租戶的獨立設定

關于這塊的内容,可以參考之前的 這篇文章 ,ABP 也為我們提供了各個租戶獨立的自定義參數在,這塊功能是由

TenantSettingManagementProvider

實作的,隻需要在設定參數值的時候提供租戶的

ProviderName

即可。

例如:

settingManager.SetAsync("WeChatIsOpen", "true", TenantSettingValueProvider.ProviderName, tenantId.ToString(), false);
           

三、總結

其他相關文章,請參閱 文章目錄 。