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

1、簡介

ABP vNext 原生支持多租戶體系,可讓開發人員快速地基於框架開發 SaaS 系統。ABP vNext 實現多租戶的思路也很是簡單,經過一個 TenantId 來分割各個租戶的數據,而且在查詢的時候使用統一的全局過濾器(相似於軟刪除)來篩選數據。html

關於多租戶體系的東西,基本定義與核心邏輯存放在 Volo.ABP.MultiTenancy 內部。針對 ASP.NET Core MVC 的集成則是由 Volo.ABP.AspNetCore.MultiTenancy 項目實現的,針對多租戶的解析都在這個項目內部。租戶數據的存儲和管理都由 Volo.ABP.TenantManagement 模塊提供,開發人員也能夠直接使用該項目快速實現多租戶功能。數據庫

2、源碼分析

2.1 啓動模塊

AbpMultiTenancyModule 模塊是啓用整個多租戶功能的核心模塊,內部只進行了一個動做,就是從配置類當中讀取多租戶的基本信息,以 JSON Provider 爲例,就須要在 appsettings.json 裏面有 Tenants 節。express

"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 默認租戶來源

這裏的數據將會做爲默認租戶來源,也就是說在確認當前租戶的時候,會從這裏面的數據與要登陸的租戶進行比較,若是不存在則不容許進行操做。json

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 同樣,將租戶信息存儲到數據庫當中。app

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 在多租戶核心庫定義的一個基本類型之一,主要是用於規定持久化一個租戶信息須要包含的屬性。dom

[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() 方法。async

下面就是咱們 ITenantResolver 的默認實現 TenantResolver,你能夠在任什麼時候候調用它。好比說你在想要得到當前租戶 Id 的時候。不過通常不推薦這樣作,由於 ABP 已經給咱們提供了 MultiTenancyMiddleware 中間件。

也就是說,在每次請求的時候,都會將這個 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 解析器做爲結果。

注意:

在使用 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;
        });
    }
}

這裏的 BasicTenantInfoTenantConfiguraton 不一樣,前者僅用於在程序當中傳遞用戶的基本信息,然後者是用於定於持久化的標準模型。

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 ?? "admin@abp.io",
            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);

3、總結

其餘相關文章,請參閱 文章目錄

相關文章
相關標籤/搜索