[Abp 源码分析]多租户体系与权限验证

点击上方蓝字关注我们

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){// 判断当前登录的用户是否拥有租户 IDif (!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);// 根据一个名称获得一个具体的功能,如果没找到则返回 NULLFeature 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, IIocManagerAccessorwhere 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 配置。

作者:myzony

出处:https://www.cnblogs.com/myzony/p/9472483.html

公众号“码侠江湖”所发表内容注明来源的,版权归原出处所有(无法查证版权的或者未注明出处的均来自网络,系转载,转载的目的在于传递更多信息,版权属于原作者。如有侵权,请联系,笔者会第一时间删除处理!

扫描二维码

获取更多精彩

码侠江湖

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305074.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

生存是一种毅力

三月初回到成都,出站时还是凌晨5点多,第一次这么早站在成都的火车站,拥挤的人群和车辆,我的神志却异常的清醒,坚持不要母亲来接我. 到家洗漱后到头就睡,一觉醒来10点多,中午的成都虽不像北京那么阳光普照但也感觉暖暖的.想起在北京训练营的日子,"枯藤老树昏鸦"每天伴…

linux truncate文件,linux系统编程:用truncate调整文件大小

truncate的使用非常简单&#xff1a;int truncate(const char *path, off_t length);参数1&#xff1a;文件名参数2: 文件需要被调整的大小length 大于 文件大小&#xff0c; 文件后面会填充空白字节或者空洞length 小于 文件大小&#xff0c; 文件多出的部分&#xff0c;会被…

如何在 ASP.Net Core 中使用 SignalR

SignalR for ASP.Net Core 是 SignalR 的浴火重生版&#xff0c;允许你在 ASP.Net Core 中实现实时通讯&#xff0c;这里的 实时 意味着双方都能快速的感知对方发来的消息&#xff0c;比如&#xff1a;一旦 server 端有需要推送的内容将会直接 push 到 client&#xff0c;这和原…

搞笑动图:这些痛,只有程序员懂…

“代码上线后又追加了新特性” “在生产环境做 hotfix” “刚调稳定的系统&#xff0c;公司叕空降了一位架构师&#xff0c;叕要重构现有系统…… ” “当程序员听客户说还在用 IE 时” “当黑客拿到 root 权限之后” “代码进入循环分支后” “项目交接&#xff0c;客户要求开…

linux内核分为子系统,Linux内核内存管理子系统分析【转】

还是那张熟悉的老图&#xff1a;Linux内核子系统简介(由七个部分组成)Linux内存管理模型&#xff1a;1. 内存管子系统职能&#xff1a;1> 管理虚拟地址与物理地址的映射2> 管理物理内存的分配2. 地址映射管理1> 虚拟地址空间分布&#xff1a;linux采用的是一种虚拟地…

spss22.0统计分析从入门到精通_数据分析最全资料:SPSS/MATLAB/SQL/SAS/EXCEL经典教材+视频教程,快速入门!...

可以说当今社会数据就是力量。数据分析能力已经成为各个行业必备的技能&#xff0c;如果能熟练掌握数据分析技能&#xff0c;不管是留学申请还是以后的就业&#xff0c;都会是一个很大的加分项。但是目前国内很少有高校专门开设数据分析课程&#xff0c;想要掌握技能大部分要靠…

可反复擦写5万次的手写板,竟然还能用橡皮擦能清除屏幕!

作为一名文艺青年&#xff0c;平时写字画画是我的一大爱好。 心情不错&#xff0c;来个创作&#xff01; 还记得我们之前介绍过的Boogie Board电子手写板吗&#xff1f; 小木用的就是它。 虽然好用&#xff0c;只不过有时候不小心画错了&#xff0c;只能重新来。 伤心啊…… 不…

[Abp 源码分析]权限验证

点击上方蓝字关注我们0.简介Abp 本身集成了一套权限验证体系&#xff0c;通过 ASP.NET Core 的过滤器与 Castle 的拦截器进行拦截请求&#xff0c;并进行权限验证。在 Abp 框架内部&#xff0c;权限分为两块&#xff0c;一个是功能(Feature)&#xff0c;一个是权限项(Permissio…

cocoscreator editbox 只允许数字_《Cocos Creator游戏实战》做一个数字调节框

当玩家购买道具的时候&#xff0c;一个个买可能会比较麻烦&#xff0c;用数字调节框的话玩家一次性就可以买好几十个了(钱够的话)。运行效果如下&#xff1a;Cocos Creator版本&#xff1a;2.2.0公号"All Codes"后台回复"数字调节框"&#xff0c;获取该项目…

为什么一流成功人士的闹钟都定在早晨5:57?

对很多人来说&#xff0c;早晨时分是一天中最匆忙的时段。近年来&#xff0c;我在研究人们如何利用时间的过程中发现&#xff0c;市场上铺天盖地的媒体教你如何利用早晨的时间。 美国国家睡眠基金会发布的一项睡眠调查结果显示&#xff0c;在工作日的早晨&#xff0c;30-50岁的…

命令行小技巧

我们平时使用Linux的时候经常遇到这样一个问题&#xff0c;举例有这样一种情况&#xff1a;执行命令 $ cp /etc/apt/sources.list /etc/apt/sources.list.bak 这里面有个问题&#xff0c;明明 /etc/apt/sources 这几个字都是一样的&#xff0c;为什么要打两遍&#xff1f;这样的…

统计学习导论_统计机器学习之扫盲导论篇

机器学习之扫盲导论篇来都来了&#xff0c;不关注一下吗&#xff1f;&#xff1f;人工智能是当下最火的词&#xff0c;而机器学习就是它的灵魂。现在超级多搞金融的人已经用到很深的机器学习模型了&#xff0c;更别提互联网企业的大佬们了&#xff0c;比如&#xff1a;(这是一篇…

[Abp 源码分析]异常处理

点击上方蓝字关注我们Abp 框架本身针对内部抛出异常进行了统一拦截&#xff0c;并且针对不同的异常也会采取不同的处理策略。在 Abp 当中主要提供了以下几种异常类型&#xff1a;异常类型描述AbpExceptionAbp 框架定义的基本异常类型&#xff0c;Abp 所有内部定义的异常类型都继…

phpstorm设置 打开文件所在目录_PDF文件在线分享并设置打开次数

公司有一些重要的PDF文档&#xff0c;怎么才能防止外泄&#xff1f;如何加密PDF文件防止被人复制了&#xff1f;怎样让PDF不能被别人拷贝啊&#xff1f; 这些问题困扰了一部分想让别人看pdf内容又担心别人泄露的人。本文用maipdf网站的例子来进行设置。首先我们打开maipdf的网站…

荐号 | 11个人工智能与大数据相关的个人、企业优质号

AlphaGo Zero都会自学了&#xff0c;作为刚刚步入AI大门的我们&#xff0c;应该如何选择合适自己的知识平台呢&#xff1f;今天小编为你甄选了几个高质量的技术公众号。 这些号更多的不是讲授枯燥的理论&#xff0c;而是从行业资讯、一线技术、应用案例、职场发展等多个角度分享…

以表达式作为参数传入SQL的存储过程中去

在开发过程中&#xff0c;需要把一句Sql 的expression作为一个参数传入Procedure中去。 在asp.net中&#xff0c;一个Search的动作&#xff0c;把用户所操作的搜索条件写成了一句表达式&#xff0c;现只需要把这句表达式传入存储过程中去&#xff0c;与存储过程中的Sql的Select…

围棋中的数学原理

围棋一向被誉为是人类大脑智慧的专利&#xff0c;围棋的走法&#xff0c;几乎和宇宙中原子数量相同&#xff0c;甚至更多&#xff0c;每回合有250种可能&#xff0c;一盘棋可以长达150个回合&#xff0c;共有1后面再加360个0种下法&#xff0c;这样的计算量&#xff0c;对计算机…

spring boot 缓存_Spring Boot 集成 Redis 实现数据缓存

Spring Boot 集成 Redis 实现数据缓存&#xff0c;只要添加一些注解方法&#xff0c;就可以动态的去操作缓存了&#xff0c;减少代码的操作。在这个例子中我使用的是 Redis&#xff0c;其实缓存类型还有很多&#xff0c;例如 Ecache、Mamercache、Caffeine 等。Redis 简介Redi…

.md是什么文件_Element-UI源码阅读之md显示到页面

入口文件首先&#xff0c;看一个项目的入口&#xff0c;可以从package.json中去看它的运行命令 可以看到dev那行&#xff0c;执行了很多脚本&#xff1a; npm run bootstrap && npm run build:file && cross-env NODE_ENVdevelopment webpack-dev-server --con…

linux误删ssh不上,误删openssh-server删除,复原操作

在别的节点上寻找openssh的程序有哪些&#xff1a;而我的这个节点上只有一个openssh-clines&#xff0c;所以&#xff0c;就先利用了yum install openssh-server进行安装&#xff0c;但是这个安装的是最新的 openssh-server-5.3p1-122.el6.x86_64.rpm&#xff0c;担心集群节点之…