[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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...