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