[Abp 源碼分析]十二、多租戶體系與許可權驗證

来源:https://www.cnblogs.com/myzony/archive/2018/08/14/9472483.html
-Advertisement-
Play Games

0.簡介 承接上篇文章我們會在這篇文章詳細解說一下 Abp 是如何結合 與 來實現一個完整的多租戶系統的許可權校驗的。 1.多租戶的概念 多租戶系統又被稱之為 Saas ,比如阿裡雲就是一個典型的多租戶系統,用戶本身就是一個租戶,可以在上面購買自己的 ECS 實例,並且自己的數據與其他使用者(租戶)所 ...


0.簡介

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

1.多租戶的概念

多租戶系統又被稱之為 Saas ,比如阿裡雲就是一個典型的多租戶系統,用戶本身就是一個租戶,可以在上面購買自己的 ECS 實例,並且自己的數據與其他使用者(租戶)所隔絕,兩者的數據都是不可見的。

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

1.1 單部署-單資料庫

如果你的軟體系統僅部署一個實例,並且所有租戶的數據都是存放在一個資料庫裡面的,那麼可以通過一個 TenantId (租戶 Id) 來進行數據隔離。那麼當我們執行 SELECT 操作的時候就會附加上當前登錄用戶租戶 Id 作為過濾條件,那麼查出來的數據也僅僅是當前租戶的數據,而不會查詢到其他租戶的數據。

1.2 單部署-多資料庫

Abp 還提供了另外一種方式,即為每一個租戶提供一個單獨的資料庫,在用戶登錄的時候根據用戶對應的租戶 ID,從一個資料庫連接映射表獲取到當前租戶對應的資料庫連接字元串,並且在查詢數據與寫入數據的時候,不同租戶操作的資料庫是不一樣的。

2.多租戶系統的許可權驗證

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

2.1 用戶會話狀態

基本做過網站程式開發的同學都知道用於區分每一個用戶,我們需要通過 Session 來保存當前用戶的狀態,以便進行許可權驗證或者其他操作。而 Abp 框架則為我們定義了一個統一的會話狀態介面 IAbpSession ,用於標識當前用戶的狀態。在其介面當中主要定義了三個重要的屬性,第一個 UserId (用戶 Id),第二個就是 TenantId (租戶 Id),以及用於確定當前用戶是租戶還是租主的 MultiTenancySides 屬性。

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

而針對這個方法的實現又可以扯出一大堆知識,這塊我們放在後面再進行精講,這裡我們還是主要通篇講解一下多租戶體系下的數據過濾與許可權驗證。

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 源碼分析]五、系統設置》 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.點此跳轉到總目錄


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

-Advertisement-
Play Games
更多相關文章
  • 基於廖雪峰的python零基礎學習後,自我總結。適用於有一定基礎的編程人員,對我而言,則是基於.net已有方面,通過學習,記錄自我覺得有用的地方,便於後續回顧。 主要以快速定位內容,通過直觀代碼輸入輸出結果,展示獨有的特性,更直觀表現,而不拘禁於理論描述。待以後使用中遇到坑,再來詳細闡述。 本章包含 ...
  • #include /* 二分查找條件: 1、有序序列 2、數據在數組中 */ int baseBinarySearch(int a[],int h,int k) { int low=0; int high=h; int mid =0; int NoFound = -1; while (low a[m... ...
  • vue的開發環境的搭建 不管什麼軟體我們都要去官網下載安裝,這是作為專業程式員的安全意識。 1、安裝node.js 官方下載的頁面:點擊這裡 大約展示的頁面是這樣子的!我們演示是windows 64位的安裝 關於版本的選擇,作為開發我們最好還是用已經比較穩定的版本,這樣話就算遇到坑,解決的問題的文檔 ...
  • 前言: 因為臨近金九銀十的面試旺季,所以大家都在為自己下半年的跳槽做最後的一搏,都在為想進自己理想的大廠而做最後的努力。下麵就來看看這位面試頭條的朋友在面試後的總結: 因為有白金內推所以8月13號下午就直接面了,一共三輪。面完一輪hr打電話告訴你過沒,過的話下一輪。有幸面了三面,最後hr讓我等消息, ...
  • 最近才知道, mysql從5.7版本開始,增加了新的欄位類型: json 所以在centos6.5上裝了個5.7版本作為平時測試用. 設計表的時候, 欄位類型直接選json 就像平常選varchar一樣. 插入數據的時候, 需要轉成JSON_OBJECT 以下腳本運行在python2.7 因為pyt ...
  • 代碼倉庫地址 一、介紹 Protobuf是Google旗下的一款平臺無關,語言無關,可擴展的序列化結構數據格式。所以很適合用做數據存儲和作為不同應用,不同語言之間相互通信的數據交換格式,只要實現相同的協議格式即同一proto文件被編譯成不同的語言版本,加入到各自的工程中去,這樣不同語言就可以解析其他 ...
  • 目錄: 一、函數和過程 二、再談談返回值 三、函數變數的作用域 四、課時19課後習題及答案 ****************** 一、函數和過程 ****************** Python嚴格來說,只有函數,沒有過程。此話怎講? 調用print(hello())之後列印了兩行字,第一行,我們 ...
  • 爬: 爬一個網站需要幾步? 確定用戶的需求 根據需求,尋找網址 讀取網頁 urllib requests 定位並提取數據 存儲數據 mysql redis 文件存儲 爬取百度首頁:(確定用戶需求) cookie和session之間的愛情故事: 啥是cookie: 當你在瀏覽網站的時候,WEB 伺服器 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...