[Abp 源碼分析]12、多租戶體系與權限驗證

0.簡介

承接上篇文章咱們會在這篇文章詳細解說一下 Abp 是如何結合 IPermissionCheckerIFeatureChecker 來實現一個完整的多租戶系統的權限校驗的。html

1.多租戶的概念

多租戶系統又被稱之爲 Saas ,好比阿里雲就是一個典型的多租戶系統,用戶自己就是一個租戶,能夠在上面購買本身的 ECS 實例,而且本身的數據與其餘使用者(租戶)所隔絕,二者的數據都是不可見的。git

那麼 Abp 是如何實現數據隔離的呢?github

1.1 單部署-單數據庫

若是你的軟件系統僅部署一個實例,而且全部租戶的數據都是存放在一個數據庫裏面的,那麼能夠經過一個 TenantId (租戶 Id) 來進行數據隔離。那麼當咱們執行 SELECT 操做的時候就會附加上當前登陸用戶租戶 Id 做爲過濾條件,那麼查出來的數據也僅僅是當前租戶的數據,而不會查詢到其餘租戶的數據。數據庫

1.2 單部署-多數據庫

Abp 還提供了另一種方式,即爲每個租戶提供一個單獨的數據庫,在用戶登陸的時候根據用戶對應的租戶 ID,從一個數據庫鏈接映射表獲取到當前租戶對應的數據庫鏈接字符串,而且在查詢數據與寫入數據的時候,不一樣租戶操做的數據庫是不同的。express

2.多租戶系統的權限驗證

從上一篇文章咱們知道了在權限過濾器與權限攔截器當中,最終會使用 IFeatureCheckerIPermissionChecker 來進行權限校驗,而且它還持久一個用戶會話狀態 IAbpSession 用於存儲識別當前訪問網站的用戶是誰。緩存

2.1 用戶會話狀態

基本作過網站程序開發的同窗都知道用於區分每個用戶,咱們須要經過 Session 來保存當前用戶的狀態,以便進行權限驗證或者其餘操做。而 Abp 框架則爲咱們定義了一個統一的會話狀態接口 IAbpSession ,用於標識當前用戶的狀態。在其接口當中主要定義了三個重要的屬性,第一個 UserId (用戶 Id),第二個就是 TenantId (租戶 Id),以及用於肯定當前用戶是租戶仍是租主的 MultiTenancySides 屬性。框架

除此以外,還擁有一個 Use() 方法,用戶在某些時候臨時替換掉當前用戶的 UserIdTenantId 的值,這個方法在個人 《Abp + Grpc 如何實現用戶會話狀態傳遞》 文章當中有講到過。async

而針對這個方法的實現又能夠扯出一大堆知識,這塊咱們放在後面再進行精講,這裏咱們仍是主要通篇講解一下多租戶體系下的數據過濾與權限驗證。ide

2.1.1 默認會話狀態的實現

IAbpSession 當中的值默認是從 JWT 當中取得的,這取決於它的默認實現 ClaimsAbpSession,它還繼承了一個抽象父類 AbpSessionBase ,這個父類主要是實現了 Use() 方法,這裏略過。函數

在其默認實現裏面,重載了 UserIdTenantId 的獲取方法。

public override long? UserId
{
    get
    {
        // ... 其餘代碼
        var userIdClaim = PrincipalAccessor.Principal?.Claims.FirstOrDefault(c => c.Type == AbpClaimTypes.UserId);
        // ... 其餘代碼
        
        long userId;
        if (!long.TryParse(userIdClaim.Value, out userId)) return null;

        return userId;
    }
}

能夠看到這裏是經過 PrincipalAccessor 從當前請求的請求頭中獲取 Token ,並從 Claims 裏面獲取 Type 值爲 AbpClaimTypes.UserId 的對象,將其轉換爲 long 類型的 UserId,這樣就拿到了當前用戶登陸的 Id 了。

2.1.2 獲取當前請求的用戶狀態

這裏的 PrincipalAccessor 是一個 IPrincipalAccessor 接口,在 ASP .NET Core 庫當中他的實現名字叫作 AspNetCorePrincipalAccessor。其實你應該猜獲得,在這個類的構造函數當中,注入了 HttpContext 的訪問器對象 IHttpContextAccessor,這樣 IAbpSession 就能夠垂手可得地得到當前請求上下文當中的具體數據了。

public class AspNetCorePrincipalAccessor : DefaultPrincipalAccessor
{
    public override ClaimsPrincipal Principal => _httpContextAccessor.HttpContext?.User ?? base.Principal;

    private readonly IHttpContextAccessor _httpContextAccessor;

    public AspNetCorePrincipalAccessor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
}

2.1.3 小結

因此,Abp 經過 IAbpSession 能夠輕鬆地知道咱們當前用戶的狀態,包括用戶 Id 與租戶 Id,它只須要知道這兩個東西,就能夠很簡單的在 IFeatureCheckerIPermissionChecker 當中來查詢用戶所綁定的權限來進行驗證。

2.2 功能(Feature)

首先咱們的思緒回到上一章所講的 AuthorizationHelper 類,在其 AuthorizeAsync() 方法當中,使用 IFeatureChecker 來檢測用戶是否擁有某種功能。

public virtual async Task AuthorizeAsync(MethodInfo methodInfo, Type type)
{
    // 檢測功能
    await CheckFeatures(methodInfo, type);
    // 檢測權限
    await CheckPermissions(methodInfo, type);
}

而後呢,在 IFeatureChecker.CheckFeatures() 方法的內部,跟 IPermissionChecker 的套路同樣,這裏仍然是一個擴展方法,遍歷方法/類上標記的 [RequiresFeatureAttribute] 特性,調用 IFeatureCheckerGetValueAsync() 方法傳入功能的名稱,而後將其值與 "true" 相比較,爲真則是啓用了該功能,其餘值則說明沒有啓用。

public static async Task<bool> IsEnabledAsync(this IFeatureChecker featureChecker, string featureName)
{
    // 檢查是否啓用
    return string.Equals(await featureChecker.GetValueAsync(featureName), "true", StringComparison.OrdinalIgnoreCase);
}

IFeatureChecker 的定義:

public interface IFeatureChecker
{
    // 傳入功能名字,獲取真這對於當前租戶其默認值
    Task<string> GetValueAsync(string name);

    // 傳入租戶 Id 與功能名字,獲取針對於指定 Id 租戶的默認值
    Task<string> GetValueAsync(int tenantId, string name);
}

到這一步咱們仍然是跟 IFeatureChecker 打交道,那麼他的具體實現是怎樣的呢?

先來看一下這個 IFeatureChecker 的依賴關係圖:

目前看起來仍是比較簡單,他擁有一個默認實現 FeatureChecker ,其中 IFeatureValueStore 從名字就能夠知道它是用來存儲功能列表的,而 IFeatureManager 則是用來管理這些功能的,Feature 則是這些功能的定義。

結合以前在 IsEnabledAsync() 方法的調用,能夠看到它先進入的 GetValueAsync(string name) 方法,判斷當前用戶的租戶 Id 是否有值,若是沒有值則直接拋出異常,中斷權限驗證。若是有值得話,傳入當前登陸用戶的租戶 Id ,從 IFeatureManager 當中獲取到定義的權限,以後呢從 IFeatureValueStore 當中拿到功能具體的值,由於功能是針對租戶而言的,因此一個功能針對於多個租戶的值確定是不一樣的,因此在這裏查詢具體值的時候須要傳入租戶 Id。

public class FeatureChecker : IFeatureChecker, ITransientDependency
{
    public IAbpSession AbpSession { get; set; }

    public IFeatureValueStore FeatureValueStore { get; set; }

    private readonly IFeatureManager _featureManager;

    public FeatureChecker(IFeatureManager featureManager)
    {
        _featureManager = featureManager;

        FeatureValueStore = NullFeatureValueStore.Instance;
        AbpSession = NullAbpSession.Instance;
    }

    public Task<string> GetValueAsync(string name)
    {
        // 判斷當前登陸的用戶是否擁有租戶 ID
        if (!AbpSession.TenantId.HasValue)
        {
            throw new AbpException("FeatureChecker can not get a feature value by name. TenantId is not set in the IAbpSession!");
        }

       // 傳入當前登陸用戶的租戶 Id ,獲取其值
        return GetValueAsync(AbpSession.TenantId.Value, name);
    }

    public async Task<string> GetValueAsync(int tenantId, string name)
    {
        // 從功能管理器根據名字查詢用戶定義的功能
        var feature = _featureManager.Get(name);

        // 得到功能的值,若是沒有值則返回其默認值
        var value = await FeatureValueStore.GetValueOrNullAsync(tenantId, feature);
        if (value == null)
        {
            return feature.DefaultValue;
        }

        return value;
    }
}

聰明的你確定猜到功能實際上是用戶在代碼當中定義的,而功能的值則是存放在數據庫當中,每一個租戶其值都是不同的。這是否是讓你想到了系列文章 《[Abp 源碼分析]5、系統設置》 SettingProvider 的實現呢?

So,這裏的 IFeatureStore 的默認實現確定是從數據庫進行配置咯~

2.2.1 功能的定義

首先功能、權限都是樹形結構,他們均可以擁有本身的子節點,這樣能夠直接實現針對父節點賦值而擁有其子節點的全部權限。這裏先來看一下功能的的基本定義:

public class Feature
{
    // 附加數據的一個索引器
    public object this[string key]
    {
        get => Attributes.GetOrDefault(key);
        set => Attributes[key] = value;
    }

    // 功能的附加數據
    public IDictionary<string, object> Attributes { get; private set; }

    // 父級功能
    public Feature Parent { get; private set; }

    // 功能的名稱
    public string Name { get; private set; }

    // 功能的展現名稱,這是一個本地化字符串
    public ILocalizableString DisplayName { get; set; }

    // 功能的描述,同樣的是一個本地化字符串
    public ILocalizableString Description { get; set; }
    
    // 功能的輸入類型
    public IInputType InputType { get; set; }

    // 功能的默認值
    public string DefaultValue { get; set; }

    // 功能所適用的範圍
    public FeatureScopes Scope { get; set; }

    // 若是當前功能的子節點的不可變集合
    public IReadOnlyList<Feature> Children => _children.ToImmutableList();

    private readonly List<Feature> _children;

    public Feature(string name, string defaultValue, ILocalizableString displayName = null, ILocalizableString description = null, FeatureScopes scope = FeatureScopes.All, IInputType inputType = null)
    {
        Name = name ?? throw new ArgumentNullException("name");
        DisplayName = displayName;
        Description = description;
        Scope = scope;
        DefaultValue = defaultValue;
        InputType = inputType ?? new CheckboxInputType();

        _children = new List<Feature>();
        Attributes = new Dictionary<string, object>();
    }

    public Feature CreateChildFeature(string name, string defaultValue, ILocalizableString displayName = null, ILocalizableString description = null, FeatureScopes scope = FeatureScopes.All, IInputType inputType = null)
    {
        var feature = new Feature(name, defaultValue, displayName, description, scope, inputType) { Parent = this };
        _children.Add(feature);
        return feature;
    }

    public override string ToString()
    {
        return string.Format("[Feature: {0}]", Name);
    }
}

這玩意兒光看着頭仍是有點疼的,其實就是關於功能的基礎定義,他爲啥附帶了一個附加描述字典,由於能夠存儲一些額外的信息,好比說一個短信功能,他的配額和到期時間,至於他的 Scope 則說明了它的生效範圍。

2.2.2 功能管理器

接着看看 GetValueAsync(int tenantId, string name) 方法的第一句:

var feature = _featureManager.Get(name);

emmm,我要從 IFeatureManager 根據權限名稱取得一個具體的 Feature 對象,那咱們繼續來看一下 IFeatureManager 接口。

public interface IFeatureManager
{
    // 根據名稱得到一個具體的功能,這個名稱應該是惟一的
    Feature Get(string name);

    // 根據一個名稱得到一個具體的功能,若是沒找到則返回 NULL
    Feature GetOrNull(string name);

    // 得到全部定義的功能
    IReadOnlyList<Feature> GetAll();
}

2.2.3 功能管理器實現

在看具體實現的時候,咱們先不慌,先看一下它實現類所繼承的東西。

internal class FeatureManager : FeatureDefinitionContextBase, IFeatureManager, ISingletonDependency

WTF,他又繼承了什麼奇奇怪怪的東西。咱們又在此來到 FeatureDefinitionContextBase ,通過一番探查總算知道這玩意兒實現自 IFeatureDefinitionContext,看看他的定義:

// 功能定義上下文,主要功能是提供給 FeatureProvider 來建立功能的
public interface IFeatureDefinitionContext
{
    // 建立一個功能
    Feature Create(string name, string defaultValue, ILocalizableString displayName = null, ILocalizableString description = null, FeatureScopes scope = FeatureScopes.All, IInputType inputType = null);

    // 根據名稱得到一個功能
    Feature GetOrNull(string name);

    // 移除一個功能
    void Remove(string name);
}

因此,你要把這些功能存放在哪些地方呢?

其實看到這個玩意兒 name-value,答案呼之欲出,其實現內部確定是用的一個字典來存儲數據的。

接着咱們來到了 FeatureDefinitionContextBase 的默認實現 FeatureDefinitionContextBase,而後發現裏面也是別有洞天,Abp 又把字典再次封裝了一遍,此次字典的名字叫作 FeatureDictionary,你只須要記住他只提供了一個做用,就是將字典內部的全部功能項與其子功能項按照平級關係存放在字典當中。

除了內部封裝了一個字典以外,在這個上下文當中,實現了建立,獲取,和移除功能的方法,而後就沒有了。咱們再次回到功能管理器,

功能管理器集成了這個上下文基類,集合以前 IFeatureManager 所定義的接口,它就具有了隨時能夠修改功能集的權力。那麼這些功能是何時被定義的,而又是何時被初始化到這個字典的呢?

在前面咱們已經說過,Feature 的增長與以前文章所講的系統設置是同樣的,他們都是經過集成一個 Provider ,而後在模塊預加載的時候,經過一個 IFeatureConfiguration 的東西被添加到 Abp 系統當中的。因此在 FeatureManager 內部注入了 IFeatureConfiguration 用來拿到用戶在模塊加載時所配置的功能項集合。

public interface IFeatureConfiguration
{
    /// <summary>
    /// Used to add/remove <see cref="FeatureProvider"/>s.
    /// </summary>
    ITypeList<FeatureProvider> Providers { get; }
}

下面給你演示一下如何添加一個功能項:

public class AppFeatureProvider : FeatureProvider
{
    public override void SetFeatures(IFeatureDefinitionContext context)
    {
        var sampleBooleanFeature = context.Create("SampleBooleanFeature", defaultValue: "false");
        sampleBooleanFeature.CreateChildFeature("SampleNumericFeature", defaultValue: "10");
        context.Create("SampleSelectionFeature", defaultValue: "B");
    }
}

不用猜想 FeatureProvier 的實現了,他就是一個抽象類,定義了一個 SetFeatures 方法好讓你實現而已。

以後我又在模塊的預加載方法吧 AppFeatureProvider 添加到了IFeatureConfiguration 裏面:

public class XXXModule : AbpModule
{
    public override void PreInitialize()
    {
        Configuration.Features.Providers.Add<AppFeatureProvider>();
    }
}

而功能管理器則是在 Abp 核心模塊 AbpKernalModule 初始化的時候,跟着權限管理器和系統設置管理器,一塊兒被初始化了。

public override void PostInitialize()
{
    RegisterMissingComponents();

    // 這裏是系統的設置的管理器
    IocManager.Resolve<SettingDefinitionManager>().Initialize();
    // 功能管理器在這裏
    IocManager.Resolve<FeatureManager>().Initialize();
    // 權限管理器
    IocManager.Resolve<PermissionManager>().Initialize();
    IocManager.Resolve<LocalizationManager>().Initialize();
    IocManager.Resolve<NotificationDefinitionManager>().Initialize();
    IocManager.Resolve<NavigationManager>().Initialize();

    if (Configuration.BackgroundJobs.IsJobExecutionEnabled)
    {
        var workerManager = IocManager.Resolve<IBackgroundWorkerManager>();
        workerManager.Start();
        workerManager.Add(IocManager.Resolve<IBackgroundJobManager>());
    }
}

看看功能管理器的定義就知道了:

public void Initialize()
{
    foreach (var providerType in _featureConfiguration.Providers)
    {
        using (var provider = CreateProvider(providerType))
        {
            provider.Object.SetFeatures(this);
        }
    }

    Features.AddAllFeatures();
}

波瀾不驚的我早已看透一切,能夠看到這裏他經過遍歷注入的 FeatureProvider 集合,傳入本身,讓他們能夠向本身注入定義的功能項。

2.2.4 功能的存儲

繼續看 IFeatureChecker 的代碼,最後從功能管理器拿到了功能項以後,就要根據租戶的 Id 取得它具體的值了。值還能存在哪兒,除了數據庫最合適放這種東西,其餘的你願意也能夠存在 TXT 裏面。

public interface IFeatureValueStore
{
    // 很簡潔,你傳入當前用戶的租戶 Id 與 當前須要校驗的功能項,我給你他的值
    Task<string> GetValueOrNullAsync(int tenantId, Feature feature);
}

廢話很少說,來到 Zero 關於這個功能存儲類的定義 AbpFeatureValueStore<TTenant,TUser>,你先不着急看那兩個泛型參數,這兩個泛型就是你的用戶與租戶實體,咱們先看看這玩意兒繼承了啥東西:

public class AbpFeatureValueStore<TTenant, TUser> :
        IAbpZeroFeatureValueStore,
        ITransientDependency,
        IEventHandler<EntityChangedEventData<Edition>>,
        IEventHandler<EntityChangedEventData<EditionFeatureSetting>>

        where TTenant : AbpTenant<TUser>
        where TUser : AbpUserBase

能夠看到它首先繼承了 IAbpZeroFeatureValueStore 接口,這裏的 IAbpZeroFeatureValueStore 接口同樣的繼承的 IFeatureValueStore,因此在 Abp 底層框架可以直接使用。

其次咱們還看到它監聽了兩個實體變動事件,也就是 Edition 與 EditFeatureSettings 表產生變化的時候,會進入到本類進行處理,其實這裏的處理就是發生改變以後,拿到改變實體的 Id,從緩存清除掉髒數據而已。

而後咱們直奔主題,找到方法的實現:

public virtual Task<string> GetValueOrNullAsync(int tenantId, Feature feature)
{
    return GetValueOrNullAsync(tenantId, feature.Name);
}

發現又是一個空殼子,繼續跳轉:

public virtual async Task<string> GetValueOrNullAsync(int tenantId, string featureName)
{
    // 首先從租戶功能值表獲取功能的值
    var cacheItem = await GetTenantFeatureCacheItemAsync(tenantId);
    // 得到到值
    var value = cacheItem.FeatureValues.GetOrDefault(featureName);
    // 不等於空,優先獲取租戶的值而忽略掉版本的值
    if (value != null)
    {
        return value;
    }

    // 若是租戶功能值表的緩存說我還有版本 Id,那麼就去版本級別的功能值表查找功能的值
    if (cacheItem.EditionId.HasValue)
    {
        value = await GetEditionValueOrNullAsync(cacheItem.EditionId.Value, featureName);
        if (value != null)
        {
            return value;
        }
    }

    return null;
}

這纔是真正的獲取功能值的地方,其他方法就再也不詳細講述,這兩個從緩存獲取的方法,都分別有一個工廠方法從數據庫拿去數據的,因此你也不用擔憂緩存裏面不存在值的狀況。

2.2.5 小結

總的來講功能是針對租戶的一個權限,Abp 建議一個父母功能通常定義爲 布爾功能。只有父母功能可用時,子功能纔可用。ABP不強制這樣作,可是建議這樣作。

在一個基於 Abp 框架的系統功能權限是可選的,具體使用仍是取決於你所開發的業務系統是否有這種需求。

2.3 權限(Permission)

2.3.1 權限的定義

權限的定義與 Feature 同樣,都是存放了一些基本信息,好比說權限的惟一標識,權限的展現名稱與描述,只不過少了 Feature 的附加屬性而已。下面咱們就會加快進度來講明一下權限相關的知識。

2.3.2 權限檢測器

權限相比於功能,權限更加細化到了用戶與角色,角色經過與權限關聯,角色就是一個權限組的集合,用戶再跟角色進行關聯。看看權限管理器的定義吧:

public abstract class PermissionChecker<TRole, TUser> : IPermissionChecker, ITransientDependency, IIocManagerAccessor
        where TRole : AbpRole<TUser>, new()
        where TUser : AbpUser<TUser>

仍是相對而言比較簡單的,在這裏你只須要關注兩個東西:

public virtual async Task<bool> IsGrantedAsync(string permissionName)
{
    return AbpSession.UserId.HasValue && await _userManager.IsGrantedAsync(AbpSession.UserId.Value, permissionName);
}

public virtual async Task<bool> IsGrantedAsync(long userId, string permissionName)
{
    return await _userManager.IsGrantedAsync(userId, permissionName);
}

這就是權限校驗的實現,第一個是傳入當前用戶的 Id 扔到 _userManager 進行校驗,而第二個則扔一個用戶制定的 Id 進行校驗。

看到這裏,咱們又該到下一節了,講解一下這個 _userManager 是何方神聖。

2.3.3 用戶管理器

若是讀者接觸過 ASP.NET Core MVC 的 Identity 確定對於 UserManager<,> 不會陌生,沒錯,這裏的 _userManager 就是繼承自 UserManager<TUser, long>, 實現的 AbpUserManager<TRole, TUser>

繼續咱們仍是看關鍵方法 IsGrantedAsync()

public virtual async Task<bool> IsGrantedAsync(long userId, string permissionName)
{
    // 傳入用戶 ID 與須要檢測的權限,經過權限管理器得到 Permission 對象
    return await IsGrantedAsync(
        userId,
        _permissionManager.GetPermission(permissionName)
    );
}

仍是個空殼子,繼續跳轉:

public virtual async Task<bool> IsGrantedAsync(long userId, Permission permission)
{
    // 首先檢測當前用戶是否擁有租戶信息
    if (!permission.MultiTenancySides.HasFlag(GetCurrentMultiTenancySide()))
    {
        return false;
    }

    // 而後檢測權限依賴的功能,若是功能沒有啓用,同樣的是沒權限的
    if (permission.FeatureDependency != null && GetCurrentMultiTenancySide() == MultiTenancySides.Tenant)
    {
        FeatureDependencyContext.TenantId = GetCurrentTenantId();

        if (!await permission.FeatureDependency.IsSatisfiedAsync(FeatureDependencyContext))
        {
            return false;
        }
    }

    // 得到當前用戶所擁有的權限,沒有權限同樣滾蛋
    var cacheItem = await GetUserPermissionCacheItemAsync(userId);
    if (cacheItem == null)
    {
        return false;
    }

    // 檢測當前用戶是否被授予了特許權限,沒有的話則直接跳過,有的話說明這是個特權用戶,擁有這個特殊權限
    if (cacheItem.GrantedPermissions.Contains(permission.Name))
    {
        return true;
    }

    // 檢測禁用權限名單中是否擁有本權限,若是有,同樣的不經過
    if (cacheItem.ProhibitedPermissions.Contains(permission.Name))
    {
        return false;
    }

    // 檢測用戶角色是否擁有改權限
    foreach (var roleId in cacheItem.RoleIds)
    {
        if (await RoleManager.IsGrantedAsync(roleId, permission))
        {
            return true;
        }
    }

    return false;
}

這裏咱們沒有講解權限管理器與權限的注入是由於他們兩個簡直一毛同樣好吧,你能夠看看權限的定義:

public class MyAuthorizationProvider : AuthorizationProvider
{
    public override void SetPermissions(IPermissionDefinitionContext context)
    {
        var administration = context.CreatePermission("Administration");

        var userManagement = administration.CreateChildPermission("Administration.UserManagement");
        userManagement.CreateChildPermission("Administration.UserManagement.CreateUser");

        var roleManagement = administration.CreateChildPermission("Administration.RoleManagement");
    }
}

是否是感受跟功能的 Provider 很像...

2.3.4 小結

權限僅僅會與用於和角色掛鉤,與租戶無關,它和功能的實現大同小異,可是也是值得咱們借鑑學習的。

3.多租戶數據過濾

租戶與租戶之間是如何進行數據過濾的呢?

這裏簡單講一下單部署-單數據庫的作法吧,在 EF Core 當中針對每個實體都提供了一個全局過濾的方法 HasQueryFilter,有了這個東西,在每次 EF Core 進行查詢的時候都會將查詢表達式附加上你自定義的過濾器一塊兒進行查詢。

在 Abp 內部定義了一個藉口,叫作 IMustHaveTenant,這玩意兒有一個必須實現的屬性 TenantId,因此只要在你的實體繼承了該接口,確定就是會有 TenantId 字段咯,那麼 Abp 就能夠先判斷你當前的實體是否實現了 IMusHaveTenant 接口,若是有的話,就給你建立了一個過濾器拼接到你的查詢表達式當中。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // DbContext 模型建立的時候
    base.OnModelCreating(modelBuilder);

    // 遍歷全部 DbContext 定義的實體
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        ConfigureGlobalFiltersMethodInfo
            .MakeGenericMethod(entityType.ClrType)
            .Invoke(this, new object[] { modelBuilder, entityType });
    }
}

protected void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType)
where TEntity : class
{
    // 判斷實體是否實現了租戶或者軟刪除接口,實現了則添加一個過濾器
    if (entityType.BaseType == null && ShouldFilterEntity<TEntity>(entityType))
    {
        var filterExpression = CreateFilterExpression<TEntity>();
        if (filterExpression != null)
        {
            modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
        }
    }
}

// 數據過濾用的查詢表達式構建
protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
    where TEntity : class
{
    Expression<Func<TEntity, bool>> expression = null;

    if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
    {
        /* This condition should normally be defined as below:
            * !IsSoftDeleteFilterEnabled || !((ISoftDelete) e).IsDeleted
            * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
            * So, we made a workaround to make it working. It works same as above.
            */
        Expression<Func<TEntity, bool>> softDeleteFilter = e => !((ISoftDelete)e).IsDeleted || ((ISoftDelete)e).IsDeleted != IsSoftDeleteFilterEnabled;
        expression = expression == null ? softDeleteFilter : CombineExpressions(expression, softDeleteFilter);
    }

    if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
    {
        /* This condition should normally be defined as below:
            * !IsMayHaveTenantFilterEnabled || ((IMayHaveTenant)e).TenantId == CurrentTenantId
            * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
            * So, we made a workaround to make it working. It works same as above.
            */
        Expression<Func<TEntity, bool>> mayHaveTenantFilter = e => ((IMayHaveTenant)e).TenantId == CurrentTenantId || (((IMayHaveTenant)e).TenantId == CurrentTenantId) == IsMayHaveTenantFilterEnabled;
        expression = expression == null ? mayHaveTenantFilter : CombineExpressions(expression, mayHaveTenantFilter);
    }

    if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
    {
        /* This condition should normally be defined as below:
            * !IsMustHaveTenantFilterEnabled || ((IMustHaveTenant)e).TenantId == CurrentTenantId
            * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502)
            * So, we made a workaround to make it working. It works same as above.
            */
        Expression<Func<TEntity, bool>> mustHaveTenantFilter = e => ((IMustHaveTenant)e).TenantId == CurrentTenantId || (((IMustHaveTenant)e).TenantId == CurrentTenantId) == IsMustHaveTenantFilterEnabled;
        expression = expression == null ? mustHaveTenantFilter : CombineExpressions(expression, mustHaveTenantFilter);
    }

    return expression;
}

上面就是實現了,你每次使用 EF Core 查詢某個表的實體都會應用這個過濾表達式。

3.1 禁用過濾

可是能夠看到在建立表達式的時候這裏還有一些諸如 IsSoftDeleteFilterEnabled 的東西,這個就是用於你在某些時候須要禁用掉軟刪除過濾器的時候所須要用到的。

看看是哪兒來的:

protected virtual bool IsSoftDeleteFilterEnabled => CurrentUnitOfWorkProvider?.Current?.IsFilterEnabled(AbpDataFilters.SoftDelete) == true;

能夠看到這個玩意兒是使用當前的工做單元來進行控制的,檢測當前工做單元的過濾器是否被啓用,若是實體被打了軟刪除接口,而且被啓用的話,那麼就執行過濾,反之亦然。

這些過濾器都是放在 AbpDataFilters 當中的,如今有如下幾種定義:

public static class AbpDataFilters
{
    public const string SoftDelete = "SoftDelete";

    public const string MustHaveTenant = "MustHaveTenant";

    public const string MayHaveTenant = "MayHaveTenant";

    public static class Parameters
    {
        public const string TenantId = "tenantId";
    }
}

而這些過濾器是在 AbpKernelModule 的預加載方法當中被添加到 UOW 的默認配置當中的。

public override void PreInitialize()
{
    // ... 其餘代碼
    AddUnitOfWorkFilters();
    // ... 其餘代碼
}

private void AddUnitOfWorkFilters()
{
    Configuration.UnitOfWork.RegisterFilter(AbpDataFilters.SoftDelete, true);
    Configuration.UnitOfWork.RegisterFilter(AbpDataFilters.MustHaveTenant, true);
    Configuration.UnitOfWork.RegisterFilter(AbpDataFilters.MayHaveTenant, true);
}

這些東西被添加到了 IUnitOfWorkDefaultOptions 以後,每次初始化一個工做單元,其自帶的 Filiters 都是從這個 IUnitOfWorkDefaultOptions 拿到的,除非用戶顯式指定 UowOptions 配置。

4.點此跳轉到總目錄

相關文章
相關標籤/搜索