主頁 > .NET開發 > [Abp vNext 原始碼分析] - 19. 多租戶

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

2020-09-16 20:36:16 .NET開發

一、簡介

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

三、總結

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/59648.html

標籤:.NET Core

上一篇:Asp.Net Core Filter 深入淺出的那些事-AOP

下一篇:C#實作-瀏覽器UA決議獲得手機、系統、瀏覽器等資訊

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more