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

来源:https://www.cnblogs.com/myzony/archive/2020/03/03/12401352.html
-Advertisement-
Play Games

一、簡介 ABP vNext 原生支持多租戶體系,可以讓開發人員快速地基於框架開發 SaaS 系統。ABP vNext 實現多租戶的思路也非常簡單,通過一個 來分割各個租戶的數據,並且在查詢的時候使用統一的全局過濾器( 類似於軟刪除 )來篩選數據。 關於多租戶體系的東西,基本定義與核心邏輯存放在 V ...


一、簡介

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 中間件。

也就是說,在每次請求的時候,都會將這個 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 ?? "[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);

三、總結

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Bottle是一個快速、簡潔、輕量級的基於WSIG的微型Web框架,此框架只由一個 .py 文件,除了Python的標準庫外,其不依賴任何其他模塊。 1 pip install bottle 2 easy_install bottle 3 apt-get install python-bottle ...
  • [TOC] std::copy是C++標準庫中的演算法介面,主要用於兩個容器間的複製,據說其效率要優於自己用for迴圈逐個複製。之前一直非常混淆其中的用法,這裡總結了幾個例子如下: cpp include include include include using namespace std; int ...
  • Python裡面如何拷貝一個對象? http://blog.csdn.net/sharkw/article/details/1934090 標準庫中的copy模塊提供了兩個方法來實現拷貝.一個方法是copy,它返回和參數包含內容一樣的對象. 使用deepcopy方法,對象中的屬性也被覆制 Pytho ...
  • 使用 resx 文件,可以動態切換語言,使用如下類: public class LanguageManager : INotifyPropertyChanged { private readonly ResourceManager _resourceManager; private static r ...
  • 1、簡述 private、 protected、 public、 internal 修飾符的訪問許可權。private : 私有成員, 在類的內部才可以訪問。protected : 保護成員,該類內部和繼承類中可以訪問。public : 公共成員,完全公開,沒有訪問限制。internal: 當前程式集 ...
  • 創建動畫面臨的第一個挑戰是為動畫選擇正確的屬性。期望的結果(例如,在視窗中移動元素)與需要使用的屬性(在這種情況下是Canvas.Left和Canvas.Top屬性)之間的關係並不總是很直觀。下麵是一些指導原則: 如果希望使用動畫來使元素顯示和消失,不要使用Visibility屬性(該屬性只能在完全 ...
  • 在.NET4.0之前,如果我們需要在多線程環境下使用Dictionary類,除了自己實現線程同步來保證線程安全外,我們沒有其他選擇。很多開發人員肯定都實現過類似的線程安全方案,可能是通過創建全新的線程安全字典,或者僅是簡單的用一個類封裝一個Dictionary對象,併在所有方法中加上鎖機制,我們稱這 ...
  • 前言 最近一直奔波於面試,面了幾家公司的研發。有讓我受益頗多的面試經驗,也有讓我感覺浪費時間的面試經歷~因為疫情原因,最近宅在家裡也沒事,就想著使用Redis配合事件匯流排去實現下具體的業務。 需求 一個簡單的電商,有幾個重要的需求點 商品下單後TODO 存儲訂單信息 鎖定商品庫存 消息推送商家端 訂 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...