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 配置。