IT教程 ·

[Abp vNext 源码剖析] – 19. 多租户

使用Taiko + Gauge进行自动化测试(一)

一、简介

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. 多租户 IT教程 第1张

也就是说,在每次要求的时刻,都会将这个 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);

三、总结

其他相干文章,请参阅 。

Redis 中的过期元素是如何被处理的?视频+图文版给你答案——面试突击 002 期

参与评论