0.簡介 承接上篇文章我們會在這篇文章詳細解說一下 Abp 是如何結合 與 來實現一個完整的多租戶系統的許可權校驗的。 1.多租戶的概念 多租戶系統又被稱之為 Saas ,比如阿裡雲就是一個典型的多租戶系統,用戶本身就是一個租戶,可以在上面購買自己的 ECS 實例,並且自己的數據與其他使用者(租戶)所 ...
0.簡介
承接上篇文章我們會在這篇文章詳細解說一下 Abp 是如何結合 IPermissionChecker
與 IFeatureChecker
來實現一個完整的多租戶系統的許可權校驗的。
1.多租戶的概念
多租戶系統又被稱之為 Saas ,比如阿裡雲就是一個典型的多租戶系統,用戶本身就是一個租戶,可以在上面購買自己的 ECS 實例,並且自己的數據與其他使用者(租戶)所隔絕,兩者的數據都是不可見的。
那麼 Abp 是如何實現數據隔離的呢?
1.1 單部署-單資料庫
如果你的軟體系統僅部署一個實例,並且所有租戶的數據都是存放在一個資料庫裡面的,那麼可以通過一個 TenantId
(租戶 Id) 來進行數據隔離。那麼當我們執行 SELECT 操作的時候就會附加上當前登錄用戶租戶 Id 作為過濾條件,那麼查出來的數據也僅僅是當前租戶的數據,而不會查詢到其他租戶的數據。
1.2 單部署-多資料庫
Abp 還提供了另外一種方式,即為每一個租戶提供一個單獨的資料庫,在用戶登錄的時候根據用戶對應的租戶 ID,從一個資料庫連接映射表獲取到當前租戶對應的資料庫連接字元串,並且在查詢數據與寫入數據的時候,不同租戶操作的資料庫是不一樣的。
2.多租戶系統的許可權驗證
從上一篇文章我們知道了在許可權過濾器與許可權攔截器當中,最終會使用 IFeatureChecker
與 IPermissionChecker
來進行許可權校驗,並且它還持久一個用戶會話狀態 IAbpSession
用於存儲識別當前訪問網站的用戶是誰。
2.1 用戶會話狀態
基本做過網站程式開發的同學都知道用於區分每一個用戶,我們需要通過 Session 來保存當前用戶的狀態,以便進行許可權驗證或者其他操作。而 Abp 框架則為我們定義了一個統一的會話狀態介面 IAbpSession
,用於標識當前用戶的狀態。在其介面當中主要定義了三個重要的屬性,第一個 UserId
(用戶 Id),第二個就是 TenantId
(租戶 Id),以及用於確定當前用戶是租戶還是租主的 MultiTenancySides
屬性。
除此之外,還擁有一個 Use()
方法,用戶在某些時候臨時替換掉當前用戶的 UserId
與 TenantId
的值,這個方法在我的 《Abp + Grpc 如何實現用戶會話狀態傳遞》 文章當中有講到過。
而針對這個方法的實現又可以扯出一大堆知識,這塊我們放在後面再進行精講,這裡我們還是主要通篇講解一下多租戶體系下的數據過濾與許可權驗證。
2.1.1 預設會話狀態的實現
IAbpSession
當中的值預設是從 JWT 當中取得的,這取決於它的預設實現 ClaimsAbpSession
,它還繼承了一個抽象父類 AbpSessionBase
,這個父類主要是實現了 Use()
方法,這裡略過。
在其預設實現裡面,重載了 UserId
與 TenantId
的獲取方法。
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,它只需要知道這兩個東西,就可以很簡單的在 IFeatureChecker
和 IPermissionChecker
當中來查詢用戶所綁定的許可權來進行驗證。
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]
特性,調用 IFeatureChecker
的 GetValueAsync()
方法傳入功能的名稱,然後將其值與 "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 配置。